From 92af3a41715fe303d0b6b50877ee28308d62faad Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Tue, 24 Mar 2026 14:59:59 -0700 Subject: [PATCH 1/9] chore: Remove graduated feature flags (performance-spans-new-ui, performance-vitals-standalone-cls-lcp, starfish-mobile-appstart, statistical-detectors-rca-spans-only) (#111117) These flags are fully rolled out. Remove the flag registrations and simplify all gated code paths to use the graduated behavior directly rather than leaving behind trivially-true variables. --------- Co-authored-by: Claude Opus 4.6 --- src/sentry/features/temporary.py | 8 --- .../aggregateSpanDiff.spec.tsx | 55 +++++++++++++------ .../aggregateSpanDiff.tsx | 8 +-- .../pageOverviewWebVitalsDetailPanel.spec.tsx | 8 +-- .../pageOverviewWebVitalsDetailPanel.tsx | 43 ++++----------- .../insights/pages/mobile/am1OverviewPage.tsx | 10 ++-- .../pages/mobile/mobileOverviewPage.tsx | 10 ++-- .../details/span/sections/description.tsx | 8 +-- 8 files changed, 66 insertions(+), 84 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index c4d90b51361673..19a3e13758c6cd 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -216,11 +216,7 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:performance-transaction-name-only-search", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the EAP-powered transactions summary view manager.add("organizations:performance-transaction-summary-eap", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enables the new UI for span summary and the spans tab - manager.add("organizations:performance-spans-new-ui", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) manager.add("organizations:performance-use-metrics", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True) - # Enable standalone cls and lcp in the web vitals module - manager.add("organizations:performance-vitals-standalone-cls-lcp", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Suggestions for Web Vitals Module manager.add("organizations:performance-web-vitals-seer-suggestions", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the warning banner to inform users of pending deprecation of the transactions dataset @@ -361,14 +357,10 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:sentry-app-webhook-requests", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable standalone span ingestion manager.add("organizations:standalone-span-ingestion", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) - # Enable mobile starfish app start module view - manager.add("organizations:starfish-mobile-appstart", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable mobile starfish ui module view manager.add("organizations:starfish-mobile-ui-module", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable the new experimental starfish view manager.add("organizations:starfish-view", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable UI for regression issues RCA using spans data - manager.add("organizations:statistical-detectors-rca-spans-only", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Allow organizations to configure all symbol sources. manager.add("organizations:symbol-sources", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=True) # Enable static ClickHouse sampling for `OrganizationTagsEndpoint` diff --git a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.spec.tsx b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.spec.tsx index 5060356aad8c0c..c4c5694d9a2661 100644 --- a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.spec.tsx +++ b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.spec.tsx @@ -30,20 +30,23 @@ const defaultProject = ProjectFixture(); describe('AggregateSpanDiff', () => { beforeEach(() => { MockApiClient.clearMockResponses(); + // Spans endpoint is always queried first (primary data source) MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-root-cause-analysis/', - body: [ - { - span_op: 'db', - span_group: 'abc123', - span_description: 'SELECT * FROM users', - score: 0.9, - p95_before: 10.0, - p95_after: 20.0, - spm_before: 100.0, - spm_after: 90.0, - }, - ], + url: '/organizations/org-slug/events/', + body: { + data: [ + { + 'span.op': 'db', + 'span.group': 'abc123', + 'span.description': 'SELECT * FROM users', + [`regression_score(span.self_time,${BREAKPOINT_TIMESTAMP})`]: 0.9, + [`avg_by_timestamp(span.self_time,less,${BREAKPOINT_TIMESTAMP})`]: 10000, + [`avg_by_timestamp(span.self_time,greater,${BREAKPOINT_TIMESTAMP})`]: 20000, + [`epm_by_timestamp(less,${BREAKPOINT_TIMESTAMP})`]: 100.0, + [`epm_by_timestamp(greater,${BREAKPOINT_TIMESTAMP})`]: 90.0, + }, + ], + }, }); }); @@ -57,8 +60,8 @@ describe('AggregateSpanDiff', () => { it('renders empty state when no spans are returned', async () => { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-root-cause-analysis/', - body: [], + url: '/organizations/org-slug/events/', + body: {data: []}, }); render(); @@ -68,16 +71,32 @@ describe('AggregateSpanDiff', () => { ).toBeInTheDocument(); }); - it('renders error state when request fails', async () => { + it('falls back to RCA endpoint when spans query fails', async () => { MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ - url: '/organizations/org-slug/events-root-cause-analysis/', + url: '/organizations/org-slug/events/', statusCode: 500, body: {detail: 'Internal Server Error'}, }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/events-root-cause-analysis/', + body: [ + { + span_op: 'db', + span_group: 'abc123', + span_description: 'SELECT * FROM users', + score: 0.9, + p95_before: 10.0, + p95_after: 20.0, + spm_before: 100.0, + spm_after: 90.0, + }, + ], + }); render(); - expect(await screen.findByText(/events-root-cause-analysis/)).toBeInTheDocument(); + expect(await screen.findByText('SELECT * FROM users')).toBeInTheDocument(); + expect(screen.getByText('db')).toBeInTheDocument(); }); }); diff --git a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx index a89f7c2b83f78f..67ebf508806ca5 100644 --- a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx +++ b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx @@ -81,9 +81,6 @@ interface AggregateSpanDiffProps { export function AggregateSpanDiff({event, project}: AggregateSpanDiffProps) { const organization = useOrganization(); const location = useLocation(); - const isSpansOnly = organization.features.includes( - 'statistical-detectors-rca-spans-only' - ); const [causeType, setCauseType] = useState<'duration' | 'throughput'>('duration'); @@ -126,7 +123,6 @@ export function AggregateSpanDiff({event, project}: AggregateSpanDiffProps) { ], sorts: [{field: `regression_score(span.self_time,${breakpoint})`, kind: 'desc'}], limit: 10, - enabled: isSpansOnly, }, 'api.insights.transactions.statistical-detector-root-cause-analysis' ); @@ -141,12 +137,12 @@ export function AggregateSpanDiff({event, project}: AggregateSpanDiffProps) { end: endISO, breakpoint: new Date(breakpoint * 1000).toISOString(), projectId: project.id, - enabled: !isSpansOnly || isSpansDataError, + enabled: isSpansDataError, }); // The spans dataset may reject some legacy RCA fields/functions for certain orgs. // When that happens, fall back to the RCA endpoint so this section still renders. - const shouldUseSpansData = isSpansOnly && !isSpansDataError; + const shouldUseSpansData = !isSpansDataError; const tableData = useMemo(() => { if (shouldUseSpansData) { diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx index d31434d4184885..06126fa4eacb1b 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.spec.tsx @@ -72,17 +72,17 @@ describe('PageOverviewWebVitalsDetailPanel', () => { }); it('renders correctly with web vital', async () => { - render(, { + render(, { organization, }); await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); - expect(screen.getAllByText('Largest Contentful Paint (P75)')).toHaveLength(2); + expect(screen.getAllByText('First Contentful Paint (P75)')).toHaveLength(2); expect(screen.getByText('Transaction')).toBeInTheDocument(); expect(screen.getByText('Profile')).toBeInTheDocument(); expect(screen.getByText('Replay')).toBeInTheDocument(); - expect(screen.getByText('lcp')).toBeInTheDocument(); - expect(screen.getByText('lcp Score')).toBeInTheDocument(); + expect(screen.getByText('fcp')).toBeInTheDocument(); + expect(screen.getByText('fcp Score')).toBeInTheDocument(); }); }); diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index a5322e543b4bf3..f16eabe6deb4dc 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -92,10 +92,6 @@ export function PageOverviewWebVitalsDetailPanel({ const browserTypes = decodeBrowserTypes(location.query[SpanFields.BROWSER_NAME]); const subregions = location.query[SpanFields.USER_GEO_SUBREGION] as SubregionCode[]; const isSpansWebVital = defined(webVital) && ['inp', 'cls', 'lcp'].includes(webVital); - const isInp = webVital === 'inp'; - const useSpansWebVitals = organization.features.includes( - 'performance-vitals-standalone-cls-lcp' - ); const replayLinkGenerator = generateReplayLink(routes); @@ -346,33 +342,18 @@ export function PageOverviewWebVitalsDetailPanel({ )} - {isInp ? ( - - ) : ( - - )} + diff --git a/static/app/views/insights/pages/mobile/am1OverviewPage.tsx b/static/app/views/insights/pages/mobile/am1OverviewPage.tsx index 18a6def8378693..4773346ca4d2eb 100644 --- a/static/app/views/insights/pages/mobile/am1OverviewPage.tsx +++ b/static/app/views/insights/pages/mobile/am1OverviewPage.tsx @@ -156,12 +156,10 @@ export function Am1MobileOverviewPage({datePageFilterProps}: Am1MobileOverviewPa if (organization.features.includes('insight-modules')) { doubleChartRowCharts[0] = PerformanceWidgetSetting.SLOW_SCREENS_BY_TTID; } - if (organization.features.includes('starfish-mobile-appstart')) { - doubleChartRowCharts.push( - PerformanceWidgetSetting.SLOW_SCREENS_BY_COLD_START, - PerformanceWidgetSetting.SLOW_SCREENS_BY_WARM_START - ); - } + doubleChartRowCharts.push( + PerformanceWidgetSetting.SLOW_SCREENS_BY_COLD_START, + PerformanceWidgetSetting.SLOW_SCREENS_BY_WARM_START + ); if (organization.features.includes('insight-modules')) { doubleChartRowCharts.push(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS); diff --git a/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx b/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx index 36abcac295d991..85feb873abfbca 100644 --- a/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx +++ b/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx @@ -172,12 +172,10 @@ function EAPMobileOverviewPage({datePageFilterProps}: EAPMobileOverviewPageProps if (organization.features.includes('insight-modules')) { doubleChartRowCharts[0] = PerformanceWidgetSetting.SLOW_SCREENS_BY_TTID; } - if (organization.features.includes('starfish-mobile-appstart')) { - doubleChartRowCharts.push( - PerformanceWidgetSetting.SLOW_SCREENS_BY_COLD_START, - PerformanceWidgetSetting.SLOW_SCREENS_BY_WARM_START - ); - } + doubleChartRowCharts.push( + PerformanceWidgetSetting.SLOW_SCREENS_BY_COLD_START, + PerformanceWidgetSetting.SLOW_SCREENS_BY_WARM_START + ); if (organization.features.includes('insight-modules')) { doubleChartRowCharts.push(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS); diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx index e05fcac52b6331..4d6395d034ca7d 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx @@ -89,12 +89,10 @@ export function SpanDescription({ return formatter.toString(span.description ?? ''); }, [span.description, resolvedModule, span.sentry_tags?.description, system]); - const hasNewSpansUIFlag = - organization.features.includes('performance-spans-new-ui') && - organization.features.includes('insight-modules'); + const hasInsightModules = organization.features.includes('insight-modules'); // The new spans UI relies on the group hash assigned by Relay, which is different from the hash available on the span itself - const groupHash = hasNewSpansUIFlag + const groupHash = hasInsightModules ? (span.sentry_tags?.group ?? '') : (span.hash ?? ''); const showAction = hasExploreEnabled ? !!span.description : !!span.op && !!span.hash; @@ -159,7 +157,7 @@ export function SpanDescription({ )} - ) : hasNewSpansUIFlag && + ) : hasInsightModules && resolvedModule === ModuleName.RESOURCE && span.op === 'resource.img' ? ( From 4f5d90da5fb3053af1d937f9b7400157488c382e Mon Sep 17 00:00:00 2001 From: Gabe Villalobos Date: Tue, 24 Mar 2026 15:02:37 -0700 Subject: [PATCH 2/9] chore(eco): Adds codeowner-coverage dev dependency (#111469) --- pyproject.toml | 1 + uv.lock | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 23ee17f75a183b..b87f14a46bfe49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,6 +143,7 @@ default = true [dependency-groups] dev = [ + "codeowners-coverage>=0.2.1", "covdefaults>=2.3.0", "devservices>=1.2.4", "docker>=7.1.0", diff --git a/uv.lock b/uv.lock index 1041e4bc62556c..a3bf1abf1a20ff 100644 --- a/uv.lock +++ b/uv.lock @@ -189,6 +189,20 @@ wheels = [ { url = "https://pypi.devinfra.sentry.io/wheels/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" }, ] +[[package]] +name = "codeowners-coverage" +version = "0.3.0" +source = { registry = "https://pypi.devinfra.sentry.io/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pathspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://pypi.devinfra.sentry.io/wheels/codeowners_coverage-0.3.0-py3-none-any.whl", hash = "sha256:d30abe67e36cdcee67b75f6364b04f61b382816e5040c84c3f85b0a1e96d2508" }, +] + [[package]] name = "confluent-kafka" version = "2.8.0" @@ -1470,10 +1484,10 @@ wheels = [ [[package]] name = "pathspec" -version = "0.9.0" +version = "1.0.4" source = { registry = "https://pypi.devinfra.sentry.io/simple" } wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a" }, + { url = "https://pypi.devinfra.sentry.io/wheels/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" }, ] [[package]] @@ -2212,6 +2226,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "codeowners-coverage", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "covdefaults", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "devservices", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2378,6 +2393,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "codeowners-coverage", specifier = ">=0.2.1" }, { name = "covdefaults", specifier = ">=2.3.0" }, { name = "devservices", specifier = ">=1.2.4" }, { name = "django-stubs", specifier = ">=5.2.9" }, From 282f1e93775faa8fa3eef8412661b311de573975 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:17:47 -0700 Subject: [PATCH 3/9] ref: Migrate ThreadPoolExecutor to ContextPropagatingThreadPoolExecutor (#111460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical import swap of `concurrent.futures.ThreadPoolExecutor` to `sentry.utils.concurrent.ContextPropagatingThreadPoolExecutor` across 22 files. This ensures `contextvars.Context` (including Sentry SDK scopes and OpenTelemetry trace context) is automatically propagated to worker threads. The wrapper (added in #111451) overrides `submit()` to call `contextvars.copy_context().run()`, which is the same pattern used in production in Seer's `ContextAwareThreadPoolExecutor`. No behavior change — the wrapper is a strict superset of the base class. This covers all "zero risk" usages — scoped `with` blocks where the parent blocks on results. Remaining long-lived instance executors (Kafka consumers) will be migrated separately with additional review. Depends on #111451 Agent transcript: https://claudescope.sentry.dev/share/QnsOk2l0-uRvsU_onK7WIQZOUCRhaVHKc_lj_o-l8yQ --- src/sentry/api/endpoints/organization_events.py | 5 +++-- src/sentry/api/endpoints/organization_events_trace.py | 5 +++-- src/sentry/api/endpoints/organization_events_trends_v2.py | 6 ++++-- .../api/endpoints/organization_trace_item_attributes.py | 4 ++-- .../endpoints/organization_trace_item_attributes_ranked.py | 4 ++-- src/sentry/api/endpoints/organization_trace_item_stats.py | 5 +++-- src/sentry/api/endpoints/organization_trace_meta.py | 4 ++-- src/sentry/api/endpoints/organization_traces.py | 4 ++-- src/sentry/integrations/aws_lambda/integration.py | 4 ++-- src/sentry/integrations/middleware/hybrid_cloud/parser.py | 5 +++-- src/sentry/integrations/utils/issue_summary_for_alerts.py | 3 ++- src/sentry/models/files/abstractfile.py | 4 ++-- src/sentry/replays/tasks.py | 6 +++--- src/sentry/replays/usecases/delete.py | 4 ++-- src/sentry/replays/usecases/reader.py | 4 ++-- src/sentry/seer/autofix/autofix.py | 3 ++- src/sentry/seer/explorer/index_data.py | 7 +++++-- src/sentry/snuba/spans_rpc.py | 6 ++++-- src/sentry/snuba/trace.py | 6 ++++-- src/sentry/utils/snuba.py | 4 ++-- src/sentry/utils/snuba_rpc.py | 6 ++++-- 21 files changed, 58 insertions(+), 41 deletions(-) diff --git a/src/sentry/api/endpoints/organization_events.py b/src/sentry/api/endpoints/organization_events.py index fe4ebb707d0fc8..0b7d2400a6a27b 100644 --- a/src/sentry/api/endpoints/organization_events.py +++ b/src/sentry/api/endpoints/organization_events.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed from typing import Any, NotRequired, TypedDict import sentry_sdk @@ -54,6 +54,7 @@ from sentry.snuba.types import DatasetQuery from sentry.snuba.utils import RPC_DATASETS, dataset_split_decision_inferred_from_query, get_dataset from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.cursors import Cursor, EAPPageTokenCursor from sentry.utils.snuba import SnubaError @@ -409,7 +410,7 @@ def _discover_data_fn( # Unable to infer based on selected fields and query string, so run both queries. else: map = {} - with ThreadPoolExecutor(max_workers=3) as exe: + with ContextPropagatingThreadPoolExecutor(max_workers=3) as exe: futures = { exe.submit( _data_fn, dataset_query, offset, limit, scoped_query diff --git a/src/sentry/api/endpoints/organization_events_trace.py b/src/sentry/api/endpoints/organization_events_trace.py index e13ff81b50b804..a46ec41d5c3237 100644 --- a/src/sentry/api/endpoints/organization_events_trace.py +++ b/src/sentry/api/endpoints/organization_events_trace.py @@ -4,7 +4,7 @@ import logging from collections import defaultdict, deque from collections.abc import Callable, Iterable, Mapping, Sequence -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed from datetime import datetime, timedelta from typing import Any, Optional, TypedDict, TypeVar, cast @@ -38,6 +38,7 @@ from sentry.snuba.occurrences_rpc import OccurrenceCategory, Occurrences from sentry.snuba.query_sources import QuerySource from sentry.snuba.referrer import Referrer +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.numbers import base32_encode, format_grouped_length from sentry.utils.sdk import set_span_attribute from sentry.utils.snuba import bulk_snuba_queries @@ -1286,7 +1287,7 @@ def update_children(event: TraceEvent, limit: int) -> None: @staticmethod def nodestore_event_map(events: Sequence[SnubaTransaction]) -> dict[str, Event | GroupEvent]: event_map = {} - with ThreadPoolExecutor(max_workers=20) as executor: + with ContextPropagatingThreadPoolExecutor(max_workers=20) as executor: future_to_event = { executor.submit( eventstore.backend.get_event_by_id, event["project.id"], event["id"] diff --git a/src/sentry/api/endpoints/organization_events_trends_v2.py b/src/sentry/api/endpoints/organization_events_trends_v2.py index a4222c663c25e5..8407e94ad54b54 100644 --- a/src/sentry/api/endpoints/organization_events_trends_v2.py +++ b/src/sentry/api/endpoints/organization_events_trends_v2.py @@ -1,5 +1,4 @@ import logging -from concurrent.futures import ThreadPoolExecutor from functools import partial import sentry_sdk @@ -24,6 +23,7 @@ from sentry.snuba.metrics_performance import query as metrics_query from sentry.snuba.referrer import Referrer from sentry.types.ratelimit import RateLimit, RateLimitCategory +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.iterators import chunked from sentry.utils.snuba import SnubaTSResult @@ -275,7 +275,9 @@ def get_trends_data(stats_data, request): ] # send the data to microservice - with ThreadPoolExecutor(thread_name_prefix=__name__) as query_thread_pool: + with ContextPropagatingThreadPoolExecutor( + thread_name_prefix=__name__ + ) as query_thread_pool: results = list( query_thread_pool.map( partial(detect_breakpoints, viewer_context=viewer_context), diff --git a/src/sentry/api/endpoints/organization_trace_item_attributes.py b/src/sentry/api/endpoints/organization_trace_item_attributes.py index 4735b153fa5d67..29ac157b439e83 100644 --- a/src/sentry/api/endpoints/organization_trace_item_attributes.py +++ b/src/sentry/api/endpoints/organization_trace_item_attributes.py @@ -1,5 +1,4 @@ from collections.abc import Callable, Sequence -from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from typing import Any, Literal, NotRequired, TypedDict @@ -71,6 +70,7 @@ from sentry.snuba.referrer import Referrer from sentry.tagstore.types import TagValue from sentry.utils import snuba_rpc +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.cursors import Cursor, CursorResult POSSIBLE_ATTRIBUTE_TYPES = ["string", "number", "boolean"] @@ -321,7 +321,7 @@ def get(self, request: Request, organization: Organization) -> Response: def data_fn(offset: int, limit: int) -> list[TraceItemAttributeKey]: futures = [] - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=len(POSSIBLE_ATTRIBUTE_TYPES), ) as pool: diff --git a/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py b/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py index b0013e71a0c5ea..19ce04218773b3 100644 --- a/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py +++ b/src/sentry/api/endpoints/organization_trace_item_attributes_ranked.py @@ -1,6 +1,5 @@ import logging from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor from typing import Any, TypedDict, cast from rest_framework.request import Request @@ -34,6 +33,7 @@ from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans from sentry.utils import snuba_rpc +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.snuba_rpc import trace_item_stats_rpc logger = logging.getLogger(__name__) @@ -326,7 +326,7 @@ def run_table_query_with_error_handling(query_string): referrer=Referrer.API_SPAN_SAMPLE_GET_SPAN_DATA.value, ) - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=PARALLELIZATION_FACTOR * 2 + 2, # 2 cohorts * threads + 2 totals queries ) as query_thread_pool: diff --git a/src/sentry/api/endpoints/organization_trace_item_stats.py b/src/sentry/api/endpoints/organization_trace_item_stats.py index cd11ba13580cdd..76cce4f8bdd649 100644 --- a/src/sentry/api/endpoints/organization_trace_item_stats.py +++ b/src/sentry/api/endpoints/organization_trace_item_stats.py @@ -1,6 +1,6 @@ import logging from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed from rest_framework import serializers from rest_framework.request import Request @@ -28,6 +28,7 @@ from sentry.search.eap.types import SearchResolverConfig from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.cursors import Cursor, CursorResult logger = logging.getLogger(__name__) @@ -210,7 +211,7 @@ def data_fn(offset: int, limit: int): ) stats_results: dict[str, dict[str, dict]] = defaultdict(lambda: {"data": {}}) - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=MAX_THREADS, ) as query_thread_pool: diff --git a/src/sentry/api/endpoints/organization_trace_meta.py b/src/sentry/api/endpoints/organization_trace_meta.py index e23b111691876a..a097df205cf3e3 100644 --- a/src/sentry/api/endpoints/organization_trace_meta.py +++ b/src/sentry/api/endpoints/organization_trace_meta.py @@ -1,5 +1,4 @@ import logging -from concurrent.futures import ThreadPoolExecutor from typing import TypedDict import sentry_sdk @@ -28,6 +27,7 @@ from sentry.snuba.rpc_dataset_common import RPCBase, TableQuery from sentry.snuba.spans_rpc import Spans from sentry.snuba.trace import _run_uptime_results_query, _uptime_results_query +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -208,7 +208,7 @@ def get(self, request: Request, organization: Organization, trace_id: str) -> Ht # parallelizing the queries here, but ideally this parallelization happens by calling run_bulk_table_queries include_uptime = request.GET.get("include_uptime", "0") == "1" max_workers = 3 + (1 if include_uptime else 0) - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=max_workers, ) as query_thread_pool: diff --git a/src/sentry/api/endpoints/organization_traces.py b/src/sentry/api/endpoints/organization_traces.py index 7789cd953d0805..6f534e1474dccc 100644 --- a/src/sentry/api/endpoints/organization_traces.py +++ b/src/sentry/api/endpoints/organization_traces.py @@ -1,7 +1,6 @@ import dataclasses from collections import defaultdict from collections.abc import Callable, Generator, Mapping, MutableMapping -from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from datetime import timedelta from typing import Any, Literal, NotRequired, TypedDict @@ -56,6 +55,7 @@ from sentry.snuba.occurrences_rpc import OccurrenceCategory from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.numbers import clip from sentry.utils.sdk import set_span_attribute from sentry.utils.snuba import bulk_snuba_queries_with_referrers @@ -253,7 +253,7 @@ def _execute_rpc(self): # issue if len(self.snuba_params.projects) < len(all_projects) and self.offset == 0: selected_project_request = self.get_traces_rpc(list(self.snuba_params.projects)) - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=2 ) as query_thread_pool: all_project_future = query_thread_pool.submit(get_traces_rpc, all_project_request) diff --git a/src/sentry/integrations/aws_lambda/integration.py b/src/sentry/integrations/aws_lambda/integration.py index f23cb4fac9aa94..6530a7f13b495f 100644 --- a/src/sentry/integrations/aws_lambda/integration.py +++ b/src/sentry/integrations/aws_lambda/integration.py @@ -2,7 +2,6 @@ import logging from collections.abc import Mapping -from concurrent.futures import ThreadPoolExecutor from typing import TYPE_CHECKING, Any from botocore.exceptions import ClientError @@ -31,6 +30,7 @@ from sentry.silo.base import control_silo_function from sentry.users.models.user import User from sentry.users.services.user.serial import serialize_rpc_user +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.sdk import capture_exception from .client import ConfigurationError, gen_aws_client @@ -421,7 +421,7 @@ def _enable_lambda(function): failures = [] success_count = 0 - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( max_workers=options.get("aws-lambda.thread-count") ) as _lambda_setup_thread_pool: # use threading here to parallelize requests diff --git a/src/sentry/integrations/middleware/hybrid_cloud/parser.py b/src/sentry/integrations/middleware/hybrid_cloud/parser.py index 29d4b6998eb88e..581c52a2e34dc9 100644 --- a/src/sentry/integrations/middleware/hybrid_cloud/parser.py +++ b/src/sentry/integrations/middleware/hybrid_cloud/parser.py @@ -2,7 +2,7 @@ import logging from abc import ABC -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed from typing import TYPE_CHECKING, Any, ClassVar from django.conf import settings @@ -33,6 +33,7 @@ from sentry.silo.client import CellSiloClient, SiloClientError from sentry.types.cell import Cell, find_cells_for_orgs, get_cell_by_name from sentry.utils import metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.options import sample_modulo logger = logging.getLogger(__name__) @@ -147,7 +148,7 @@ def get_responses_from_cell_silos(self, cells: list[Cell]) -> dict[str, CellResu cell_to_response_map = {} - with ThreadPoolExecutor(max_workers=len(cells)) as executor: + with ContextPropagatingThreadPoolExecutor(max_workers=len(cells)) as executor: future_to_cell = { executor.submit(self.get_response_from_cell_silo, cell): cell for cell in cells } diff --git a/src/sentry/integrations/utils/issue_summary_for_alerts.py b/src/sentry/integrations/utils/issue_summary_for_alerts.py index 652a5368bfcc74..e7ef10174177cc 100644 --- a/src/sentry/integrations/utils/issue_summary_for_alerts.py +++ b/src/sentry/integrations/utils/issue_summary_for_alerts.py @@ -10,6 +10,7 @@ from sentry.seer.autofix.constants import SeerAutomationSource from sentry.seer.autofix.issue_summary import get_issue_summary from sentry.seer.autofix.utils import is_seer_scanner_rate_limited +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -47,7 +48,7 @@ def fetch_issue_summary(group: Group) -> dict[str, Any] | None: try: with sentry_sdk.start_span(op="ai_summary.fetch_issue_summary_for_alert"): - with concurrent.futures.ThreadPoolExecutor() as executor: + with ContextPropagatingThreadPoolExecutor() as executor: future = executor.submit( get_issue_summary, group, source=SeerAutomationSource.ALERT ) diff --git a/src/sentry/models/files/abstractfile.py b/src/sentry/models/files/abstractfile.py index aa4d373a86f86c..40e3c312631608 100644 --- a/src/sentry/models/files/abstractfile.py +++ b/src/sentry/models/files/abstractfile.py @@ -7,7 +7,6 @@ import os import tempfile from collections.abc import Sequence -from concurrent.futures import ThreadPoolExecutor from hashlib import sha1 from typing import TYPE_CHECKING, Any, Generic, TypeVar @@ -25,6 +24,7 @@ from sentry.models.files.abstractfileblobindex import AbstractFileBlobIndex from sentry.models.files.utils import DEFAULT_BLOB_SIZE, AssembleChecksumMismatch, nooplogger from sentry.utils import metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -104,7 +104,7 @@ def fetch_file(offset, getfile) -> None: mem[offset : offset + len(chunk)] = chunk offset += len(chunk) - with ThreadPoolExecutor(max_workers=4) as exe: + with ContextPropagatingThreadPoolExecutor(max_workers=4) as exe: for idx in self._indexes: exe.submit(fetch_file, idx.offset, idx.blob.getfile) diff --git a/src/sentry/replays/tasks.py b/src/sentry/replays/tasks.py index 38920f60b256d6..cbf0683441b9b4 100644 --- a/src/sentry/replays/tasks.py +++ b/src/sentry/replays/tasks.py @@ -1,6 +1,5 @@ from __future__ import annotations -import concurrent.futures as cf import logging from typing import Any @@ -27,6 +26,7 @@ from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import replays_tasks from sentry.utils import metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.pubsub import KafkaPublisher logger = logging.getLogger() @@ -86,7 +86,7 @@ def delete_replays_script_async( for segment in segments: rrweb_filenames.append(make_recording_filename(segment)) - with cf.ThreadPoolExecutor(max_workers=100) as pool: + with ContextPropagatingThreadPoolExecutor(max_workers=100) as pool: pool.map(_delete_if_exists, rrweb_filenames) # Backwards compatibility. Should be deleted one day. @@ -128,7 +128,7 @@ def delete_replay_recording(project_id: int, replay_id: str) -> None: direct_storage_segments.append(segment) # Issue concurrent delete requests when interacting with a remote service provider. - with cf.ThreadPoolExecutor(max_workers=100) as pool: + with ContextPropagatingThreadPoolExecutor(max_workers=100) as pool: if direct_storage_segments: pool.map(storage.delete, direct_storage_segments) diff --git a/src/sentry/replays/usecases/delete.py b/src/sentry/replays/usecases/delete.py index 17a3a68cfb484d..1367c0714ca127 100644 --- a/src/sentry/replays/usecases/delete.py +++ b/src/sentry/replays/usecases/delete.py @@ -1,6 +1,5 @@ from __future__ import annotations -import concurrent.futures as cf import functools import logging from datetime import datetime @@ -36,6 +35,7 @@ from sentry.replays.usecases.query import execute_query, handle_search_filters from sentry.replays.usecases.query.configs.aggregate import search_config as agg_search_config from sentry.seer.signed_seer_api import SeerViewerContext +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.retries import ConditionalRetryPolicy, exponential_delay from sentry.utils.snuba import ( QueryExecutionError, @@ -76,7 +76,7 @@ def delete_replays(project_id: int, replay_ids: list[str]) -> None: def delete_replay_recordings(project_id: int, row: MatchedRow) -> None: - with cf.ThreadPoolExecutor(max_workers=100) as pool: + with ContextPropagatingThreadPoolExecutor(max_workers=100) as pool: pool.map(_delete_if_exists, _make_recording_filenames(project_id, row)) diff --git a/src/sentry/replays/usecases/reader.py b/src/sentry/replays/usecases/reader.py index 5c7b8de714c154..a0f79337cf6b01 100644 --- a/src/sentry/replays/usecases/reader.py +++ b/src/sentry/replays/usecases/reader.py @@ -3,7 +3,6 @@ import uuid import zlib from collections.abc import Generator, Iterator -from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta from typing import Any @@ -29,6 +28,7 @@ from sentry.replays.lib.storage import RecordingSegmentStorageMeta, filestore, storage from sentry.replays.models import ReplayRecordingSegment from sentry.replays.usecases.pack import unpack +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.snuba import raw_snql_query # METADATA QUERY BEHAVIOR. @@ -265,7 +265,7 @@ def download_segments(segments: list[RecordingSegmentStorageMeta]) -> Iterator[b def iter_segment_data( segments: list[RecordingSegmentStorageMeta], ) -> Generator[tuple[int, memoryview]]: - with ThreadPoolExecutor(max_workers=10) as pool: + with ContextPropagatingThreadPoolExecutor(max_workers=10) as pool: segment_data = pool.map(_download_segment, segments) for i, result in enumerate(segment_data): diff --git a/src/sentry/seer/autofix/autofix.py b/src/sentry/seer/autofix/autofix.py index 302dedfad2d8ed..3dbc0005d5abee 100644 --- a/src/sentry/seer/autofix/autofix.py +++ b/src/sentry/seer/autofix/autofix.py @@ -50,6 +50,7 @@ from sentry.tasks.seer.autofix import check_autofix_status from sentry.users.models.user import User from sentry.users.services.user.model import RpcUser +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.event_frames import EventFrame logger = logging.getLogger(__name__) @@ -341,7 +342,7 @@ def _fetch_trace(): try: with sentry_sdk.start_span(op="seer.autofix.get_trace_tree_for_event"): - with concurrent.futures.ThreadPoolExecutor() as executor: + with ContextPropagatingThreadPoolExecutor() as executor: future = executor.submit(_fetch_trace) return future.result(timeout=timeout) except concurrent.futures.TimeoutError: diff --git a/src/sentry/seer/explorer/index_data.py b/src/sentry/seer/explorer/index_data.py index 9d7feef5763deb..24946149ecfac0 100644 --- a/src/sentry/seer/explorer/index_data.py +++ b/src/sentry/seer/explorer/index_data.py @@ -1,6 +1,6 @@ import logging import re -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed from datetime import UTC, datetime, timedelta from typing import Any @@ -32,6 +32,7 @@ from sentry.services.eventstore.models import Event, GroupEvent from sentry.snuba.referrer import Referrer from sentry.snuba.spans_rpc import Spans +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -396,7 +397,9 @@ def get_profiles_for_trace(trace_id: str, project_id: int) -> TraceProfiles | No for p in profile_data ] - with ThreadPoolExecutor(max_workers=min(len(profiles_to_fetch), 5)) as executor: + with ContextPropagatingThreadPoolExecutor( + max_workers=min(len(profiles_to_fetch), 5) + ) as executor: future_to_profile = { executor.submit( _fetch_and_process_profile, diff --git a/src/sentry/snuba/spans_rpc.py b/src/sentry/snuba/spans_rpc.py index 0c9bb441c82b49..7e289ef8728a40 100644 --- a/src/sentry/snuba/spans_rpc.py +++ b/src/sentry/snuba/spans_rpc.py @@ -1,7 +1,6 @@ import logging import time from collections import defaultdict -from concurrent.futures import ThreadPoolExecutor from typing import Any import sentry_sdk @@ -30,6 +29,7 @@ from sentry.search.events.types import SAMPLING_MODES, SnubaParams from sentry.snuba import rpc_dataset_common from sentry.utils import json, snuba_rpc +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger("sentry.snuba.spans_rpc") @@ -200,7 +200,9 @@ def process_item_groups(item_groups): break request.page_token.CopyFrom(response.page_token) # We want to process the spans while querying the next page - with ThreadPoolExecutor(thread_name_prefix=__name__, max_workers=2) as thread_pool: + with ContextPropagatingThreadPoolExecutor( + thread_name_prefix=__name__, max_workers=2 + ) as thread_pool: _ = thread_pool.submit(process_item_groups, response.item_groups) response_future = thread_pool.submit(snuba_rpc.get_trace_rpc, request) response = response_future.result() diff --git a/src/sentry/snuba/trace.py b/src/sentry/snuba/trace.py index 54991b0ca23d30..a346b1f055cf94 100644 --- a/src/sentry/snuba/trace.py +++ b/src/sentry/snuba/trace.py @@ -1,11 +1,11 @@ import logging from collections import defaultdict from collections.abc import Mapping -from concurrent.futures import ThreadPoolExecutor from datetime import datetime from typing import Any, Literal, NotRequired, TypedDict from sentry.uptime.subscriptions.regions import get_region_config +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -640,7 +640,9 @@ def query_trace_data( # 1 worker each for spans, errors, performance issues, and optionally uptime max_workers = 4 if include_uptime else 3 - query_thread_pool = ThreadPoolExecutor(thread_name_prefix=__name__, max_workers=max_workers) + query_thread_pool = ContextPropagatingThreadPoolExecutor( + thread_name_prefix=__name__, max_workers=max_workers + ) with query_thread_pool: spans_future = query_thread_pool.submit( Spans.run_trace_query, diff --git a/src/sentry/utils/snuba.py b/src/sentry/utils/snuba.py index e88baf3c044e18..219b7eff37e292 100644 --- a/src/sentry/utils/snuba.py +++ b/src/sentry/utils/snuba.py @@ -9,7 +9,6 @@ import time from collections import namedtuple from collections.abc import Callable, Collection, Mapping, MutableMapping, Sequence -from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from copy import deepcopy from datetime import datetime, timedelta, timezone @@ -46,6 +45,7 @@ from sentry.snuba.query_sources import QuerySource from sentry.snuba.referrer import validate_referrer from sentry.utils import json, metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.dates import outside_retention_with_modified_start logger = logging.getLogger(__name__) @@ -1237,7 +1237,7 @@ def _bulk_snuba_query(snuba_requests: Sequence[SnubaRequest]) -> ResultSet: span.set_tag("snuba.num_queries", len(snuba_requests_list)) if len(snuba_requests_list) > 1: - with ThreadPoolExecutor( + with ContextPropagatingThreadPoolExecutor( thread_name_prefix=__name__, max_workers=10, ) as query_thread_pool: diff --git a/src/sentry/utils/snuba_rpc.py b/src/sentry/utils/snuba_rpc.py index 12437cae9e7a61..88545c5578354b 100644 --- a/src/sentry/utils/snuba_rpc.py +++ b/src/sentry/utils/snuba_rpc.py @@ -2,7 +2,6 @@ import logging import os -from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from functools import partial from typing import Protocol, TypeVar @@ -48,6 +47,7 @@ from urllib3.response import BaseHTTPResponse from sentry.utils import json, metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor from sentry.utils.snuba import SnubaError, _snuba_pool logger = logging.getLogger(__name__) @@ -159,7 +159,9 @@ def _make_rpc_requests( thread_isolation_scope=sentry_sdk.get_isolation_scope(), thread_current_scope=sentry_sdk.get_current_scope(), ) - with ThreadPoolExecutor(thread_name_prefix=__name__, max_workers=10) as query_thread_pool: + with ContextPropagatingThreadPoolExecutor( + thread_name_prefix=__name__, max_workers=10 + ) as query_thread_pool: response = [ result for result in query_thread_pool.map( From 1bb80bfe0ada278416d3991ae903adb098e41310 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 24 Mar 2026 18:19:07 -0400 Subject: [PATCH 4/9] ref(scraps): update status indicator design (#111470) Updates StatusIndicator to new designs, adds subtle pulse animation https://github.com/user-attachments/assets/5c798930-fbe4-4e72-9760-4dab23274c66 --------- Co-authored-by: Cursor Agent --- .../core/statusIndicator/statusIndicator.mdx | 45 ++++++---- .../statusIndicator/statusIndicator.spec.tsx | 6 +- .../core/statusIndicator/statusIndicator.tsx | 85 +++++++++++++++---- .../views/navigation/primary/components.tsx | 2 +- 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/static/app/components/core/statusIndicator/statusIndicator.mdx b/static/app/components/core/statusIndicator/statusIndicator.mdx index c1d884a61179a2..bf59b891b5a48b 100644 --- a/static/app/components/core/statusIndicator/statusIndicator.mdx +++ b/static/app/components/core/statusIndicator/statusIndicator.mdx @@ -7,7 +7,7 @@ resources: js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/statusIndicator/statusIndicator.tsx --- -import {Flex} from '@sentry/scraps/layout'; +import {Flex, Grid} from '@sentry/scraps/layout'; import {StatusIndicator} from '@sentry/scraps/statusIndicator'; import {Text} from '@sentry/scraps/text'; @@ -15,33 +15,42 @@ import * as Storybook from 'sentry/stories'; export const documentation = import('!!type-loader!@sentry/scraps/statusIndicator'); -The `StatusIndicator` is a small circular indicator that communicates status or severity. It supports three semantic variants mapped to design tokens. +The `StatusIndicator` is a small dot indicator that communicates status or severity. It supports six semantic variants mapped to design tokens. Each dot renders with a subtle muted background pulse behind it. Placement is the caller's responsibility — the component renders as an inline element with no positioning of its own. ## Variants - - - - Info - - - - Warning - - - - Danger - - + + + Accent + + + Success + + + Warning + + + Danger + + + Promotion + + + Muted + + ```jsx - + + + + ``` ## Accessibility @@ -64,7 +73,7 @@ Omit `aria-label` when the indicator accompanies visible text that already conve // Meaningful — the indicator is the only signal - + // Live region — updates are announced to screen readers diff --git a/static/app/components/core/statusIndicator/statusIndicator.spec.tsx b/static/app/components/core/statusIndicator/statusIndicator.spec.tsx index 9c5fe52f320cb1..f7c626146e2162 100644 --- a/static/app/components/core/statusIndicator/statusIndicator.spec.tsx +++ b/static/app/components/core/statusIndicator/statusIndicator.spec.tsx @@ -4,12 +4,12 @@ import {StatusIndicator} from '@sentry/scraps/statusIndicator'; describe('StatusIndicator', () => { it('is hidden from the accessibility tree when no aria-label is provided', () => { - render(); + render(); expect(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true'); }); it('has role="img" and aria-label when aria-label is provided', () => { - render(); + render(); const dot = screen.getByRole('img', {name: 'Online'}); expect(dot).toBeInTheDocument(); expect(dot).not.toHaveAttribute('aria-hidden'); @@ -18,7 +18,7 @@ describe('StatusIndicator', () => { it('respects an explicit role override alongside aria-label', () => { render( { variant: StatusIndicatorVariant; } /** - * A small circular dot indicator that communicates status or state via color. + * A small dot indicator that communicates status or state via color. * * Accessibility: * - Provide `aria-label` when the dot conveys meaningful information not @@ -46,22 +52,37 @@ export function StatusIndicator(props: StatusIndicatorProps) { function getDotTokens( variant: StatusIndicatorVariant, theme: Theme -): {background: string; border: string} { +): {dot: string; pulse: string} { switch (variant) { + case 'accent': + return { + dot: theme.tokens.background.accent.vibrant, + pulse: theme.tokens.background.transparent.accent.muted, + }; case 'danger': return { - background: theme.tokens.background.danger.vibrant, - border: theme.tokens.border.danger.muted, + dot: theme.tokens.background.danger.vibrant, + pulse: theme.tokens.background.transparent.danger.muted, }; case 'warning': return { - background: theme.tokens.background.warning.vibrant, - border: theme.tokens.border.warning.muted, + dot: theme.tokens.background.warning.vibrant, + pulse: theme.tokens.background.transparent.warning.muted, + }; + case 'success': + return { + dot: theme.tokens.background.success.vibrant, + pulse: theme.tokens.background.transparent.success.muted, }; - case 'info': + case 'promotion': return { - background: theme.tokens.background.accent.vibrant, - border: theme.tokens.border.accent.muted, + dot: theme.tokens.background.promotion.vibrant, + pulse: theme.tokens.background.transparent.promotion.muted, + }; + case 'muted': + return { + dot: theme.tokens.graphics.neutral.moderate, + pulse: theme.tokens.background.transparent.neutral.muted, }; default: unreachable(variant); @@ -69,10 +90,42 @@ function getDotTokens( } } +const gentlePulse = keyframes` + 0% { + opacity: 0; + border-radius: 6px; + transform: scale(0.9) rotate(0); + } + 75% { + opacity: 1; + border-radius: 3px; + transform: scale(1) rotate(1turn); + } + 100% { + opacity: 0; + border-radius: 3px; + transform: scale(1.25) rotate(1turn); + } +`; + const Dot = styled('span')<{variant: StatusIndicatorVariant}>` - border-radius: 50%; - width: 10px; - height: 10px; - background-color: ${p => getDotTokens(p.variant, p.theme).background}; - border: 2px solid ${p => getDotTokens(p.variant, p.theme).border}; + position: relative; + isolation: isolate; + border-radius: ${p => p.theme.radius.xs}; + width: 8px; + height: 8px; + background-color: ${p => getDotTokens(p.variant, p.theme).dot}; + + &::before { + content: ''; + position: absolute; + z-index: -1; + top: -2px; + left: -2px; + width: 12px; + height: 12px; + border-radius: ${p => p.theme.radius['2xs']}; + background-color: ${p => getDotTokens(p.variant, p.theme).pulse}; + animation: ${gentlePulse} 2.2s cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite; + } `; diff --git a/static/app/views/navigation/primary/components.tsx b/static/app/views/navigation/primary/components.tsx index 7940cc827fb69f..88f9856f69e1e9 100644 --- a/static/app/views/navigation/primary/components.tsx +++ b/static/app/views/navigation/primary/components.tsx @@ -316,7 +316,7 @@ function PrimaryNavigationUnreadIndicator({ {p => ( )} From 1c4f77348408d7cbdb822afa60ddea38b5b471c3 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:20:46 -0700 Subject: [PATCH 5/9] ref(ai-search): cleanup redundant ai flag checks (#111453) gen-ai access is already checked in https://github.com/getsentry/sentry/blob/master/static/app/components/searchQueryBuilder/context.tsx#L142-L145 --- .../discover/results/issueListSeerComboBox.tsx | 7 +------ .../results/resultsSearchQueryBuilder.tsx | 15 ++++++--------- static/app/views/explore/logs/logsTab.tsx | 14 ++++++-------- .../views/explore/logs/logsTabSeerComboBox.tsx | 7 +------ .../views/explore/spans/spansTabSearchSection.tsx | 7 +------ .../views/explore/spans/spansTabSeerComboBox.tsx | 7 +------ static/app/views/issueList/issueSearch.tsx | 12 +++++------- 7 files changed, 21 insertions(+), 48 deletions(-) diff --git a/static/app/views/discover/results/issueListSeerComboBox.tsx b/static/app/views/discover/results/issueListSeerComboBox.tsx index 140af06374af5e..b145accf051a3b 100644 --- a/static/app/views/discover/results/issueListSeerComboBox.tsx +++ b/static/app/views/discover/results/issueListSeerComboBox.tsx @@ -295,12 +295,7 @@ export function IssueListSeerComboBox({onSearch}: IssueListSeerComboBoxProps) { ] ); - const areAiFeaturesAllowed = - enableAISearch && - !organization?.hideAiFeatures && - organization.features.includes('gen-ai-features'); - - if (!areAiFeaturesAllowed) { + if (!enableAISearch) { return null; } diff --git a/static/app/views/discover/results/resultsSearchQueryBuilder.tsx b/static/app/views/discover/results/resultsSearchQueryBuilder.tsx index e4316c61b57e60..6b77b7cc0a588e 100644 --- a/static/app/views/discover/results/resultsSearchQueryBuilder.tsx +++ b/static/app/views/discover/results/resultsSearchQueryBuilder.tsx @@ -143,8 +143,6 @@ export function ResultsSearchQueryBuilder(props: Props) { includeTransactions = true, } = props; - const organization = useOrganization(); - const placeholderText = useMemo(() => { return placeholder ?? t('Search for events, users, tags, and more'); }, [placeholder]); @@ -160,13 +158,12 @@ export function ResultsSearchQueryBuilder(props: Props) { includeTransactions, }); - // AI search is only enabled for Errors dataset + // AI search is only enabled for Errors dataset if translate endpoint is enabled const isErrorsDataset = dataset === DiscoverDatasets.ERRORS; - const areAiFeaturesAllowed = - isErrorsDataset && - !organization?.hideAiFeatures && - organization.features.includes('gen-ai-features') && - organization.features.includes('gen-ai-search-agent-translate'); + const organization = useOrganization(); + const hasTranslateEndpoint = organization.features.includes( + 'gen-ai-search-agent-translate' + ); const searchBarProps = { placeholderText, @@ -187,7 +184,7 @@ export function ResultsSearchQueryBuilder(props: Props) { return ( diff --git a/static/app/views/explore/logs/logsTabSeerComboBox.tsx b/static/app/views/explore/logs/logsTabSeerComboBox.tsx index 557db18c83d831..1b43b7565adcfd 100644 --- a/static/app/views/explore/logs/logsTabSeerComboBox.tsx +++ b/static/app/views/explore/logs/logsTabSeerComboBox.tsx @@ -259,11 +259,6 @@ export function LogsTabSeerComboBox() { ] ); - const areAiFeaturesAllowed = - enableAISearch && - !organization?.hideAiFeatures && - organization.features.includes('gen-ai-features'); - const usePollingEndpoint = organization.features.includes( 'gen-ai-search-agent-translate' ); @@ -311,7 +306,7 @@ export function LogsTabSeerComboBox() { [] ); - if (!areAiFeaturesAllowed) { + if (!enableAISearch) { return null; } diff --git a/static/app/views/explore/spans/spansTabSearchSection.tsx b/static/app/views/explore/spans/spansTabSearchSection.tsx index dbecf633a56e32..f38ba8865fb094 100644 --- a/static/app/views/explore/spans/spansTabSearchSection.tsx +++ b/static/app/views/explore/spans/spansTabSearchSection.tsx @@ -366,8 +366,6 @@ export function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSection const [caseInsensitive, setCaseInsensitive] = useCaseInsensitivity(); const organization = useOrganization(); - const areAiFeaturesAllowed = - !organization?.hideAiFeatures && organization.features.includes('gen-ai-features'); const hasRawSearchReplacement = organization.features.includes( 'search-query-builder-raw-search-replacement' ); @@ -449,10 +447,7 @@ export function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSection return ( - + tourContext={ExploreSpansTourContext} id={ExploreSpansTour.SEARCH_BAR} diff --git a/static/app/views/explore/spans/spansTabSeerComboBox.tsx b/static/app/views/explore/spans/spansTabSeerComboBox.tsx index 503d171782936d..c1382e057ceeb5 100644 --- a/static/app/views/explore/spans/spansTabSeerComboBox.tsx +++ b/static/app/views/explore/spans/spansTabSeerComboBox.tsx @@ -268,13 +268,8 @@ export function SpansTabSeerComboBox() { [askSeerSuggestedQueryRef, navigate, organization, pageFilters.selection] ); - const areAiFeaturesAllowed = - enableAISearch && - !organization?.hideAiFeatures && - organization.features.includes('gen-ai-features'); - useTraceExploreAiQuerySetup({ - enableAISearch: areAiFeaturesAllowed && !useTranslateEndpoint, + enableAISearch: enableAISearch && !useTranslateEndpoint, }); // Get selected project IDs for the polling variant diff --git a/static/app/views/issueList/issueSearch.tsx b/static/app/views/issueList/issueSearch.tsx index 33946cf80f241c..d8b31c86a53bf4 100644 --- a/static/app/views/issueList/issueSearch.tsx +++ b/static/app/views/issueList/issueSearch.tsx @@ -37,16 +37,14 @@ function IssueSearchBar({query, onSearch, className}: IssueSearchProps) { } export function IssueSearch({query, onSearch, className}: IssueSearchProps) { - const organization = useOrganization(); const {selection: pageFilters} = usePageFilters(); const {getFilterKeys, getFilterKeySections, getTagValues} = useIssueListSearchBarDataProvider({pageFilters}); - // Gate behind gen-ai-search-agent-translate (internal only) plus standard AI consent checks - const areAiFeaturesAllowed = - !organization?.hideAiFeatures && - organization.features.includes('gen-ai-features') && - organization.features.includes('gen-ai-search-agent-translate'); + const organization = useOrganization(); + const hasTranslateEndpoint = organization.features.includes( + 'gen-ai-search-agent-translate' + ); return ( Date: Tue, 24 Mar 2026 18:34:38 -0400 Subject: [PATCH 6/9] ref(codeowners): assign scraps skills to design-eng (#111481) claiming some unowned files for @getsentry/design-engineering per #110578 --- .github/CODEOWNERS | 14 ++++++++++---- .github/codeowners-coverage-baseline.txt | 13 ------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9ccb63af15f43b..2c63852ce151ea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -798,15 +798,21 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /static/app/views/settings/ @getsentry/design-engineering /static/app/views/nav/ @getsentry/design-engineering /static/app/bootstrap/ @getsentry/design-engineering - -# LLM Agent guidelines +# Config files +/figma.config.json @getsentry/design-engineering +/knip.config.ts @getsentry/design-engineering +# Agents + Skills /static/.cursor/BUGBOT.md @getsentry/design-engineering /static/CLAUDE.md @getsentry/design-engineering /static/AGENTS.md @getsentry/design-engineering -/.claude/skills/design-system/ @getsentry/design-engineering - /static/eslint/ @getsentry/design-engineering /scripts/analyze-styled.ts @getsentry/design-engineering +/.agents/skills/design-system/ @getsentry/design-engineering +/.agents/skills/migrate-frontend-forms/ @getsentry/design-engineering +/.agents/skills/generate-frontend-forms/ @getsentry/design-engineering +/.agents/skills/lint-fix/ @getsentry/design-engineering +/.agents/skills/lint-new/ @getsentry/design-engineering +/.agents/skills/react-component-documentation/ @getsentry/design-engineering ## End of Frontend Platform # Coding Workflows diff --git a/.github/codeowners-coverage-baseline.txt b/.github/codeowners-coverage-baseline.txt index e7e04e0b076873..289adc57b63de7 100644 --- a/.github/codeowners-coverage-baseline.txt +++ b/.github/codeowners-coverage-baseline.txt @@ -11,22 +11,11 @@ src/sentry/data/error-** src/sentry/locale/** -.agents/skills/design-system/SKILL.md -.agents/skills/generate-frontend-forms/SKILL.md -.agents/skills/lint-fix/SKILL.md -.agents/skills/lint-fix/references/fix-patterns.md -.agents/skills/lint-fix/references/token-taxonomy.md -.agents/skills/lint-new/SKILL.md -.agents/skills/lint-new/references/rule-archetypes.md -.agents/skills/lint-new/references/schema-patterns.md -.agents/skills/lint-new/references/style-collector-guide.md -.agents/skills/migrate-frontend-forms/SKILL.md .agents/skills/notification-platform/SKILL.md .agents/skills/notification-platform/references/custom-renderers.md .agents/skills/notification-platform/references/data-and-templates.md .agents/skills/notification-platform/references/provider-template.md .agents/skills/notification-platform/references/targets-and-sending.md -.agents/skills/react-component-documentation/SKILL.md .agents/skills/setup-dev/SKILL.md .agents/skills/setup-dev/references/orbstack-fix.md .codeowners-config.yml @@ -68,9 +57,7 @@ bin/split-silo-database bin/update-migration config/build-chartcuterie.ts config/commit-template -figma.config.json jest.config.snapshots.ts -knip.config.ts migrations_lockfile.txt rspack.config.ts src/AGENTS.md From 69b5208f74773352a6512f9ea09a595c6b1c3600 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 24 Mar 2026 15:40:21 -0700 Subject: [PATCH 7/9] ref(cmd-k): swap fuse for fzf search implementation (#111483) Swaps out fuse for fzf and implements keyword searching (previously omitted due to missing fuse.js key definition). Fixes DE-1032 --------- Co-authored-by: Claude --- .../commandPalette/ui/content.spec.tsx | 163 ++++++++++++++++++ .../ui/useCommandPaletteState.tsx | 53 +++--- 2 files changed, 193 insertions(+), 23 deletions(-) diff --git a/static/app/components/commandPalette/ui/content.spec.tsx b/static/app/components/commandPalette/ui/content.spec.tsx index 8a027cccde7bfd..5417283a57f79c 100644 --- a/static/app/components/commandPalette/ui/content.spec.tsx +++ b/static/app/components/commandPalette/ui/content.spec.tsx @@ -124,4 +124,167 @@ describe('CommandPaletteContent', () => { expect(onChild).toHaveBeenCalled(); expect(closeSpy).toHaveBeenCalledTimes(1); }); + + describe('search', () => { + it('typing a query filters results to matching items only', async () => { + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'route'); + + expect( + await screen.findByRole('option', {name: 'Go to route'}) + ).toBeInTheDocument(); + expect(screen.queryByRole('option', {name: 'Other'})).not.toBeInTheDocument(); + expect( + screen.queryByRole('option', {name: 'Parent action'}) + ).not.toBeInTheDocument(); + }); + + it('non-matching items are not shown', async () => { + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'xyzzy'); + + expect(screen.queryAllByRole('option')).toHaveLength(0); + }); + + it('clearing the query restores all top-level items', async () => { + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'route'); + expect( + await screen.findByRole('option', {name: 'Go to route'}) + ).toBeInTheDocument(); + + await userEvent.clear(input); + + expect( + await screen.findByRole('option', {name: 'Go to route'}) + ).toBeInTheDocument(); + expect(screen.getByRole('option', {name: 'Other'})).toBeInTheDocument(); + expect(screen.getByRole('option', {name: 'Parent action'})).toBeInTheDocument(); + }); + + it('child actions are hidden when query is empty', async () => { + render(); + await screen.findByRole('option', {name: 'Parent action'}); + + expect( + screen.queryByRole('option', {name: 'Child action'}) + ).not.toBeInTheDocument(); + }); + + it('child actions are directly searchable without drilling into the group', async () => { + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'child'); + + expect( + await screen.findByRole('option', {name: 'Parent action → Child action'}) + ).toBeInTheDocument(); + }); + + it('search is case-insensitive', async () => { + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'ROUTE'); + + expect( + await screen.findByRole('option', {name: 'Go to route'}) + ).toBeInTheDocument(); + }); + + it('actions are ranked by match quality — better matches appear first', async () => { + const actions: CommandPaletteAction[] = [ + { + type: 'navigate', + to: '/a/', + display: {label: 'Something with issues buried'}, + groupingKey: 'navigate', + }, + { + type: 'navigate', + to: '/b/', + display: {label: 'Issues'}, + groupingKey: 'navigate', + }, + ]; + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'issues'); + + const options = await screen.findAllByRole('option'); + expect(options[0]).toHaveAccessibleName('Issues'); + expect(options[1]).toHaveAccessibleName('Something with issues buried'); + }); + + it('top-level actions rank before child actions when both match the query', async () => { + const actions: CommandPaletteAction[] = [ + { + type: 'group', + display: {label: 'Group'}, + groupingKey: 'navigate', + actions: [{type: 'navigate', to: '/child/', display: {label: 'Issues'}}], + }, + { + type: 'navigate', + to: '/top/', + display: {label: 'Issues'}, + groupingKey: 'navigate', + }, + ]; + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'issues'); + + const options = await screen.findAllByRole('option'); + expect(options[0]).toHaveAccessibleName('Issues'); + expect(options[1]).toHaveAccessibleName('Group → Issues'); + }); + + it('actions with matching keywords are included in results', async () => { + const actions: CommandPaletteAction[] = [ + { + type: 'navigate', + to: '/shortcuts/', + display: {label: 'Keyboard shortcuts'}, + keywords: ['hotkeys', 'keybindings'], + groupingKey: 'help', + }, + ]; + render(); + const input = await screen.findByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'hotkeys'); + + expect( + await screen.findByRole('option', {name: 'Keyboard shortcuts'}) + ).toBeInTheDocument(); + }); + + it("searching within a drilled-in group filters that group's children", async () => { + const actions: CommandPaletteAction[] = [ + { + type: 'group', + display: {label: 'Theme'}, + groupingKey: 'navigate', + actions: [ + {type: 'callback', onAction: jest.fn(), display: {label: 'Light'}}, + {type: 'callback', onAction: jest.fn(), display: {label: 'Dark'}}, + ], + }, + ]; + render(); + + // Drill into the group + await userEvent.click(await screen.findByRole('option', {name: 'Theme'})); + await screen.findByRole('option', {name: 'Light'}); + + // Now type a query that only matches one child + const input = screen.getByRole('textbox', {name: 'Search commands'}); + await userEvent.type(input, 'dark'); + + expect(await screen.findByRole('option', {name: 'Dark'})).toBeInTheDocument(); + expect(screen.queryByRole('option', {name: 'Light'})).not.toBeInTheDocument(); + }); + }); }); diff --git a/static/app/components/commandPalette/ui/useCommandPaletteState.tsx b/static/app/components/commandPalette/ui/useCommandPaletteState.tsx index 54510ed40e9316..789d8e912ecc5f 100644 --- a/static/app/components/commandPalette/ui/useCommandPaletteState.tsx +++ b/static/app/components/commandPalette/ui/useCommandPaletteState.tsx @@ -1,25 +1,13 @@ import {useMemo, useState} from 'react'; -import type Fuse from 'fuse.js'; import {useCommandPaletteActions} from 'sentry/components/commandPalette/context'; import type {CommandPaletteActionWithKey} from 'sentry/components/commandPalette/types'; -import {strGetFn} from 'sentry/components/search/sources/utils'; -import {useFuzzySearch} from 'sentry/utils/fuzzySearch'; +import {fzf} from 'sentry/utils/search/fzf'; type CommandPaletteActionWithPriority = CommandPaletteActionWithKey & { priority: number; }; -const FUZZY_SEARCH_CONFIG: Fuse.IFuseOptions = { - keys: ['display.label', 'display.details'], - getFn: strGetFn, - shouldSort: true, - minMatchCharLength: 1, - includeScore: true, - threshold: 0.2, - ignoreLocation: true, -}; - /** * Brings up child actions to make them directly searchable. * @@ -84,21 +72,40 @@ export function useCommandPaletteState() { return flattenActions(actions); }, [actions, selectedAction]); - const fuseSearch = useFuzzySearch(displayedActions, FUZZY_SEARCH_CONFIG); const filteredActions = useMemo(() => { - if (!fuseSearch || query.length === 0) { + if (query.length === 0) { // Do not display child actions before search return displayedActions.filter(a => a.priority === 0); } - const fuzzyResults = fuseSearch.search(query).map(a => a.item); - const fuzzyKeys = new Set(fuzzyResults.map(a => a.key)); - const searchResults = displayedActions.filter( - a => a.groupingKey === 'search-result' && !fuzzyKeys.has(a.key) - ); - return [...fuzzyResults, ...searchResults].toSorted( - (a, b) => a.priority - b.priority + + const normalizedQuery = query.toLowerCase(); + + const scored = displayedActions.map(action => { + const label = typeof action.display.label === 'string' ? action.display.label : ''; + const details = + typeof action.display.details === 'string' ? action.display.details : ''; + const keywords = action.keywords?.join(' ') ?? ''; + const searchText = [label, details, keywords].filter(Boolean).join(' '); + const result = fzf(searchText, normalizedQuery, false); + return {action, score: result.score, matched: result.end !== -1}; + }); + + const matched = scored.filter(r => r.matched); + const unmatchedSearchResults = scored.filter( + r => !r.matched && r.action.groupingKey === 'search-result' ); - }, [fuseSearch, query, displayedActions]); + + const sortedMatches = matched.toSorted((a, b) => { + const priorityDiff = a.action.priority - b.action.priority; + if (priorityDiff !== 0) return priorityDiff; + return b.score - a.score; + }); + + return [ + ...sortedMatches.map(r => r.action), + ...unmatchedSearchResults.map(r => r.action), + ]; + }, [query, displayedActions]); return { actions: filteredActions, From a8a5621de2abf56c45c3065e4aeb45bfe9694206 Mon Sep 17 00:00:00 2001 From: Ryan Brooks Date: Tue, 24 Mar 2026 15:48:13 -0700 Subject: [PATCH 8/9] fix(snapshots): Disable CSS animations for snapshot testing (#111485) A recent flake in `radio.tsx` was caused by an animation on the checked state. After some investigation, this caused flakes in our snapshot suite as small timing differences resulted in some minor diffs in the checked state, indicating the animation wasn't completing. This will disable CSS animations in our snapshot tests to ensure stability. --- tests/js/sentry-test/snapshots/snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/sentry-test/snapshots/snapshot.ts b/tests/js/sentry-test/snapshots/snapshot.ts index 42cda35bd2f1eb..7833c66792aeb1 100644 --- a/tests/js/sentry-test/snapshots/snapshot.ts +++ b/tests/js/sentry-test/snapshots/snapshot.ts @@ -55,7 +55,7 @@ function renderToHTML(element: ReactElement): string { ${styleTags} From 70c99f2a8ce76ba9c0316ca3371bb2c755093853 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+gricha@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:16:50 -0700 Subject: [PATCH 9/9] ref: Migrate webhook delivery ThreadPoolExecutor (#111465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate `deliver_webhooks.py` to `ContextPropagatingThreadPoolExecutor`. Split out from the main migration PR (#111460) for separate review — this is a high-throughput webhook delivery path that uses `ThreadPoolExecutor` with `as_completed` to fan out webhook deliveries across cells. Context propagation here ensures Sentry SDK tracing spans are properly linked across the thread pool workers. Depends on #111460 Agent transcript: https://claudescope.sentry.dev/share/qPBlInQUSChZ__7X4OQMJ71LrObNmN7u3erNuSNh-yI --- src/sentry/hybridcloud/tasks/deliver_webhooks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sentry/hybridcloud/tasks/deliver_webhooks.py b/src/sentry/hybridcloud/tasks/deliver_webhooks.py index f80eeb41502887..e8d9d3411e47d9 100644 --- a/src/sentry/hybridcloud/tasks/deliver_webhooks.py +++ b/src/sentry/hybridcloud/tasks/deliver_webhooks.py @@ -1,6 +1,6 @@ import datetime import logging -from concurrent.futures import ThreadPoolExecutor, as_completed +from concurrent.futures import as_completed import orjson import requests @@ -35,6 +35,7 @@ from sentry.taskworker.namespaces import hybridcloud_control_tasks from sentry.types.cell import Cell, get_cell_by_name from sentry.utils import metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -494,7 +495,7 @@ def _run_parallel_delivery_batch( id__gte=payload.id, mailbox_name=payload.mailbox_name ).order_by("id") - with ThreadPoolExecutor(max_workers=worker_threads) as threadpool: + with ContextPropagatingThreadPoolExecutor(max_workers=worker_threads) as threadpool: futures = { threadpool.submit(deliver_message_parallel, record) for record in query[:worker_threads] }