Skip to content

feat(cmdk): Create dynamic command palette action registration#111495

Closed
rbro112 wants to merge 1 commit intomasterfrom
ryan/create_dynamic_command_palette_action_registration
Closed

feat(cmdk): Create dynamic command palette action registration#111495
rbro112 wants to merge 1 commit intomasterfrom
ryan/create_dynamic_command_palette_action_registration

Conversation

@rbro112
Copy link
Copy Markdown
Member

@rbro112 rbro112 commented Mar 24, 2026

Creates dynamic actions registration following the approach outlined by https://linear.app/getsentry/issue/DE-719/cmdk-add-api-for-registering-dynamic-actions

Adds the first usage of this dynamic acton with a docs search.
Screenshot 2026-03-24 at 4 45 11 PM

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Mar 24, 2026
Copy link
Copy Markdown
Member Author

rbro112 commented Mar 24, 2026

Comment on lines +63 to +71
return results
.flatMap(section => section.hits)
.slice(0, MAX_RESULTS)
.map(hit =>
makeCommandPaletteCallback({
display: {
label: stripHtml(hit.title ?? ''),
labelNode: highlightMarks(hit.title ?? ''),
details: hit.context?.context1,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Actions generated from doc search results with identical titles will have the same key, causing one to silently overwrite the other in the command palette.
Severity: MEDIUM

Suggested Fix

Update the key generation logic to ensure uniqueness. Include more information in the key, such as the documentation section or a unique identifier from the search result hit object, to prevent collisions. For example: const actionKey = ${id}:${action.type}:${hit.section}:${slugify(action.display.label)}`;.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/components/commandPalette/useDocsSearchActions.tsx#L63-L71

Potential issue: The key for command palette actions is generated by slugifying the
action's label. In `useDocsSearchActions.tsx`, actions are created from documentation
search results. If two search results from different sections (e.g., 'docs' and
'develop') have the same title, they will produce identical keys after slugification.
The reducer in `context.tsx` handles key collisions by overwriting the existing action
with the new one. This causes one of the search results to be silently dropped from the
command palette, making it inaccessible to the user without any warning.

Did we get this right? 👍 / 👎 to inform future reviews.

const MARK_PATTERN = new RegExp(`(${MARK_OPEN}.*?${MARK_CLOSE})`, 'g');

function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '');

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High

This string may still contain
<script
, which may cause an HTML element injection vulnerability.

Copilot Autofix

AI 22 days ago

General approach: replace the ad‑hoc regex-based HTML stripper with a more robust, well-tested sanitization strategy that cannot reintroduce <script or other tags via multi-character replacement quirks. Since this is a React/TypeScript frontend, a small, focused HTML sanitizer library is appropriate and avoids implementing brittle custom logic.

Best concrete fix here: import a lightweight sanitizer such as dompurify and use it to produce safe, plain text from the Algolia result fields. Specifically:

  • Add an import for DOMPurify at the top of useDocsSearchActions.tsx.
  • Rewrite stripHtml so that it:
    1. Sanitizes the input HTML with DOMPurify.
    2. Extracts text content without any tags. For a robust solution that doesn’t depend on browser DOM APIs (which can be problematic in SSR), DOMPurify provides DOMPurify.sanitize(html, {ALLOWED_TAGS: [], ALLOWED_ATTR: []}) to remove all tags, leaving only text.
  • Ensure highlightMarks continues to operate on the original Algolia-marked HTML string, but when it produces non-highlighted text fragments, it uses our new stripHtml implementation, which is now robust.

Concretely:

  • In static/app/components/commandPalette/useDocsSearchActions.tsx, modify stripHtml to call DOMPurify.sanitize(html, {ALLOWED_TAGS: [], ALLOWED_ATTR: []}) instead of using html.replace(/<[^>]*>/g, '').
  • Add an import at the top: import DOMPurify from 'dompurify';

This keeps all existing behavior (returning plain text for labels and non-highlighted fragments) but uses a correct, repeatable, and library-backed sanitization that won’t suffer from incomplete multi-character replacement issues.

Suggested changeset 1
static/app/components/commandPalette/useDocsSearchActions.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/static/app/components/commandPalette/useDocsSearchActions.tsx b/static/app/components/commandPalette/useDocsSearchActions.tsx
--- a/static/app/components/commandPalette/useDocsSearchActions.tsx
+++ b/static/app/components/commandPalette/useDocsSearchActions.tsx
@@ -1,5 +1,6 @@
 import {Fragment, useCallback, useState} from 'react';
 import {SentryGlobalSearch} from '@sentry-internal/global-search';
+import DOMPurify from 'dompurify';
 
 import {makeCommandPaletteCallback} from 'sentry/components/commandPalette/makeCommandPaletteAction';
 import type {CommandPaletteAction} from 'sentry/components/commandPalette/types';
@@ -14,7 +15,8 @@
 const MARK_PATTERN = new RegExp(`(${MARK_OPEN}.*?${MARK_CLOSE})`, 'g');
 
 function stripHtml(html: string): string {
-  return html.replace(/<[^>]*>/g, '');
+  // Use DOMPurify to remove all HTML tags and attributes, leaving only text content.
+  return DOMPurify.sanitize(html, {ALLOWED_TAGS: [], ALLOWED_ATTR: []});
 }
 
 /**
EOF
@@ -1,5 +1,6 @@
import {Fragment, useCallback, useState} from 'react';
import {SentryGlobalSearch} from '@sentry-internal/global-search';
import DOMPurify from 'dompurify';

import {makeCommandPaletteCallback} from 'sentry/components/commandPalette/makeCommandPaletteAction';
import type {CommandPaletteAction} from 'sentry/components/commandPalette/types';
@@ -14,7 +15,8 @@
const MARK_PATTERN = new RegExp(`(${MARK_OPEN}.*?${MARK_CLOSE})`, 'g');

function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '');
// Use DOMPurify to remove all HTML tags and attributes, leaving only text content.
return DOMPurify.sanitize(html, {ALLOWED_TAGS: [], ALLOWED_ATTR: []});
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
@rbro112 rbro112 changed the title Create dynamic command palette action registration feat(cmdk): Create dynamic command palette action registration Mar 24, 2026
@getsantry
Copy link
Copy Markdown
Contributor

getsantry bot commented Apr 15, 2026

This issue has gone three weeks without activity. In another week, I will close it.

But! If you comment or otherwise update it, I will reset the clock, and if you remove the label Waiting for: Community, I will leave it alone ... forever!


"A weed is but an unloved flower." ― Ella Wheeler Wilcox 🥀

@getsantry getsantry bot added the Stale label Apr 15, 2026
@rbro112 rbro112 closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants