From ea2b37819b91ffa6bf0a4bcffb17064aa0cbe799 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 30 Mar 2026 08:59:13 -0700 Subject: [PATCH 01/57] feat(nav): Add icon-only Feedback button to top navigation bar (#111647) Add feedback button to page frame component and place it to the right of Ask Seer Co-authored-by: Claude --- static/app/views/navigation/topBar.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/static/app/views/navigation/topBar.tsx b/static/app/views/navigation/topBar.tsx index 3f48b6ff23da..eb0d03a26731 100644 --- a/static/app/views/navigation/topBar.tsx +++ b/static/app/views/navigation/topBar.tsx @@ -5,6 +5,7 @@ import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {SizeProvider} from '@sentry/scraps/sizeContext'; +import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import {IconSeer} from 'sentry/icons'; import {t} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -95,6 +96,12 @@ export function TopBar() { {t('Ask Seer')} ) : null} + + {null} + From 0900dc63cec75538aa7da6ad8461920d50c10db2 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 30 Mar 2026 09:04:14 -0700 Subject: [PATCH 02/57] fix(repos): Make it clearer when repos are disabled or connected properly (#111764) Before it was really hard to scan the list of repos at a page like `/settings/integrations/github/?tab=configurations` the "enabled" and "disabled" labels look basically the same. Now we've removed some bespoke elements, and make it easier to scan the list: | List size | Before | After | | --- | --- | --- | | Short list | SCR-20260327-okra | SCR-20260327-oknu | longer | SCR-20260327-olag | SCR-20260327-okmi --- I also brought this over to the newer `/settings/repos/` page. Before we didn't show status at all, and the link to github could be broken because the integration on the remote side is disabled or deleted already. Now we replace the `0/0 repos` count and link to github with the status label. | Before | After | | --- | --- | | SCR-20260327-nuaj | SCR-20260327-ojky --- .../scmIntegrationTreeRow.tsx | 20 ++++-- .../installedIntegration.tsx | 69 +++++++------------ ...st_organization_integration_detail_view.py | 2 +- 3 files changed, 40 insertions(+), 51 deletions(-) diff --git a/static/app/components/repositories/scmIntegrationTree/scmIntegrationTreeRow.tsx b/static/app/components/repositories/scmIntegrationTree/scmIntegrationTreeRow.tsx index ffc5ca0055b6..bfbc3fca0376 100644 --- a/static/app/components/repositories/scmIntegrationTree/scmIntegrationTreeRow.tsx +++ b/static/app/components/repositories/scmIntegrationTree/scmIntegrationTreeRow.tsx @@ -1,7 +1,7 @@ -import {useEffect, useState, type CSSProperties, type ReactNode} from 'react'; +import {Fragment, useEffect, useState, type CSSProperties, type ReactNode} from 'react'; import styled from '@emotion/styled'; -import {Badge} from '@sentry/scraps/badge'; +import {Badge, Tag} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; import {Flex} from '@sentry/scraps/layout'; @@ -182,12 +182,20 @@ export function ScmIntegrationTreeRow({ {node.isReposPending ? ( + ) : node.repoCount === 0 ? ( + {t('disabled')} ) : ( - - {t('%s/%s repos connected', node.connectedRepoCount, node.repoCount)} - + + + {t( + '%s/%s repos connected', + node.connectedRepoCount, + node.repoCount + )} + + + )} - diff --git a/static/app/views/settings/organizationIntegrations/installedIntegration.tsx b/static/app/views/settings/organizationIntegrations/installedIntegration.tsx index d567a0b75bfb..9bdd5295a351 100644 --- a/static/app/views/settings/organizationIntegrations/installedIntegration.tsx +++ b/static/app/views/settings/organizationIntegrations/installedIntegration.tsx @@ -1,13 +1,13 @@ import {Component, Fragment} from 'react'; -import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Alert} from '@sentry/scraps/alert'; +import {Tag} from '@sentry/scraps/badge'; import {Button, LinkButton} from '@sentry/scraps/button'; +import {Flex} from '@sentry/scraps/layout'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Access} from 'sentry/components/acl/access'; -import {CircleIndicator} from 'sentry/components/circleIndicator'; import {Confirm} from 'sentry/components/confirm'; import {IconDelete, IconSettings, IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -188,7 +188,7 @@ export class InstalledIntegration extends Component { - & { - status: ObjectStatus; - hideTooltip?: boolean; - } -) { - const theme = useTheme(); - const {status, hideTooltip, ...p} = props; - const color = status === 'active' ? theme.tokens.content.success : theme.colors.gray400; - const inner = ( -
- - - {status === 'active' - ? t('enabled') - : status === 'pending_deletion' - ? t('pending deletion') - : status === 'disabled' - ? t('disabled') - : t('unknown')} - -
- ); - return hideTooltip ? ( - inner - ) : ( +function IntegrationStatus({ + status, + hideTooltip, +}: { + status: ObjectStatus; + hideTooltip?: boolean; +}) { + const label = { + active: {t('enabled')}, + pending_deletion: {t('pending deletion')}, + disabled: {t('disabled')}, + deletion_in_progress: {t('unknown')}, + }[status] ?? {t('unknown')}; + + return ( - {inner} + + + {label} + + ); } -const StyledIntegrationStatus = styled(IntegrationStatus)` - display: flex; - align-items: center; - color: ${p => p.theme.tokens.content.secondary}; - font-weight: light; - text-transform: capitalize; - &:before { - content: '|'; - color: ${p => p.theme.colors.gray200}; - margin-right: ${p => p.theme.space.md}; - font-weight: ${p => p.theme.font.weight.sans.regular}; - } -`; - const IntegrationStatusText = styled('div')` margin: 0 ${p => p.theme.space.sm} 0 ${p => p.theme.space.xs}; `; diff --git a/tests/acceptance/test_organization_integration_detail_view.py b/tests/acceptance/test_organization_integration_detail_view.py index 7967cd8a2510..53c5c200e108 100644 --- a/tests/acceptance/test_organization_integration_detail_view.py +++ b/tests/acceptance/test_organization_integration_detail_view.py @@ -74,5 +74,5 @@ def test_uninstallation(self) -> None: detail_view_page.uninstall() assert ( - self.browser.element('[data-test-id="integration-status"]').text == "Pending Deletion" + self.browser.element('[data-test-id="integration-status"]').text == "pending deletion" ) From cb7dd72b5d25c3fecc87a5ab897639d342933323 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:05:27 -0400 Subject: [PATCH 03/57] feat(dashboards): Add tableWidths to backend overview transactions table (#111790) Add `tableWidths` to the backend overview transactions table widget to prevent column overflow. Before image After image Refs BROWSE-475 --- .../backendOverview/backendOverview.ts | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts index 8f1a27abf912..48105ec26d72 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts @@ -1,3 +1,4 @@ +import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable'; import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; @@ -200,28 +201,31 @@ export const BACKEND_OVERVIEW_SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow( {h: 3, minH: 3} ); +const TABLE_FIELDS = [ + SpanFields.IS_STARRED_TRANSACTION, + SpanFields.REQUEST_METHOD, + SpanFields.TRANSACTION, + SpanFields.SPAN_OP, + SpanFields.PROJECT, + 'epm()', + `p50(${SpanFields.SPAN_DURATION})`, + `p95(${SpanFields.SPAN_DURATION})`, + `equation|failure_count() / count(${SpanFields.SPAN_DURATION})`, + `count_unique(${SpanFields.USER})`, + `sum(${SpanFields.SPAN_DURATION})`, +]; + const TRANSACTIONS_TABLE: Widget = { id: 'backend-overview-transactions-table', title: t('Transactions'), description: '', displayType: DisplayType.TABLE, interval: '5m', + tableWidths: TABLE_FIELDS.map(() => COL_WIDTH_UNDEFINED), queries: [ { name: '', - fields: [ - SpanFields.IS_STARRED_TRANSACTION, - SpanFields.REQUEST_METHOD, - SpanFields.TRANSACTION, - SpanFields.SPAN_OP, - SpanFields.PROJECT, - 'epm()', - `p50(${SpanFields.SPAN_DURATION})`, - `p95(${SpanFields.SPAN_DURATION})`, - `equation|failure_count() / count(${SpanFields.SPAN_DURATION})`, - `count_unique(${SpanFields.USER})`, - `sum(${SpanFields.SPAN_DURATION})`, - ], + fields: TABLE_FIELDS, aggregates: [ 'epm()', `p50(${SpanFields.SPAN_DURATION})`, From bece1ff877d3a42758fa9817d17d05b3e9c41f11 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 30 Mar 2026 12:08:13 -0400 Subject: [PATCH 04/57] feat(tracemetrics): Display metric name in aggregates table when no group by selected (#111513) Display the selected metric name as a left-aligned column in the aggregates table when no group bys are selected. Refs LOGS-630 --------- Co-authored-by: Claude Opus 4.6 --- .../metricInfoTabs/aggregatesTab.spec.tsx | 7 ++ .../metrics/metricInfoTabs/aggregatesTab.tsx | 114 ++++++++++++------ 2 files changed, 83 insertions(+), 38 deletions(-) diff --git a/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.spec.tsx b/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.spec.tsx index 7302d40ab324..fc7caafaa208 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.spec.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.spec.tsx @@ -99,6 +99,10 @@ describe('AggregatesTab', () => { // Both aggregate columns should be present expect(screen.getByRole('columnheader', {name: /sum/i})).toBeInTheDocument(); + + // Metric name column should be prepended when no group bys are selected + expect(screen.getByRole('columnheader', {name: 'Metric'}).tagName).toBe('DIV'); + expect(screen.getByRole('columnheader', {name: /avg/i}).tagName).toBe('BUTTON'); }); it('renders table with groupBys and aggregate columns', async () => { @@ -153,6 +157,9 @@ describe('AggregatesTab', () => { // Aggregate columns expect(screen.getByRole('columnheader', {name: /avg/i})).toBeInTheDocument(); expect(screen.getByRole('columnheader', {name: /p95/i})).toBeInTheDocument(); + + // Metric name column should NOT appear when group bys are selected + expect(screen.queryByRole('columnheader', {name: 'Metric'})).not.toBeInTheDocument(); }); it('shows empty state when no data is returned', async () => { diff --git a/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.tsx b/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.tsx index 232b4dc6d2c2..60a66fe0b980 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/aggregatesTab.tsx @@ -6,11 +6,13 @@ import throttle from 'lodash/throttle'; import {Tooltip} from '@sentry/scraps/tooltip'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; +import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t} from 'sentry/locale'; import {parseFunction} from 'sentry/utils/discover/fields'; import {prettifyTagKey} from 'sentry/utils/fields'; +import type {TableColumn} from 'sentry/views/discover/table/types'; import {decodeColumnOrder} from 'sentry/views/discover/utils'; import {useTopEvents} from 'sentry/views/explore/hooks/useTopEvents'; import {useTraceItemAttributeKeys} from 'sentry/views/explore/hooks/useTraceItemAttributeKeys'; @@ -25,6 +27,7 @@ import { TransparentLoadingMask, } from 'sentry/views/explore/metrics/metricInfoTabs/metricInfoTabStyles'; import type {TraceMetric} from 'sentry/views/explore/metrics/metricQuery'; +import {TraceMetricKnownFieldKey} from 'sentry/views/explore/metrics/types'; import { createTraceMetricFilter, getMetricsUnit, @@ -40,6 +43,18 @@ import {GenericWidgetEmptyStateWarning} from 'sentry/views/performance/landing/w const RESULT_LIMIT = 50; +const METRIC_NAME_COLUMN: TableColumn = { + key: TraceMetricKnownFieldKey.METRIC_NAME, + name: TraceMetricKnownFieldKey.METRIC_NAME, + type: 'string', + isSortable: false, + column: { + kind: 'field', + field: TraceMetricKnownFieldKey.METRIC_NAME, + }, + width: COL_WIDTH_UNDEFINED, +}; + interface AggregatesTabProps { traceMetric: TraceMetric; isMetricOptionsEmpty?: boolean; @@ -86,8 +101,24 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab const meta = result.meta ?? {}; - const groupByFieldCount = groupBys.length; - const aggregateFieldCount = fields.length - groupByFieldCount; + // When no group bys are selected, prepend the metric name as a virtual group-by column + const displayFields = useMemo(() => { + if (groupBys.length === 0) { + return [TraceMetricKnownFieldKey.METRIC_NAME, ...fields]; + } + return fields; + }, [groupBys.length, fields]); + + const displayColumns = useMemo(() => { + if (groupBys.length === 0) { + return [METRIC_NAME_COLUMN, ...columns]; + } + return columns; + }, [groupBys.length, columns]); + + // Include the virtual metric name column in the group-by count so grid/divider logic works + const groupByFieldCount = groupBys.length === 0 ? 1 : groupBys.length; + const aggregateFieldCount = displayFields.length - groupByFieldCount; const tableStyle = useMemo(() => { // First aggregate column gets 1fr, pushing remaining aggregates to the right @@ -98,23 +129,21 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab aggregateFieldCount > 1 ? `1fr repeat(${aggregateFieldCount - 1}, auto)` : '1fr'; return {gridTemplateColumns: `${groupByColumns} ${aggregateColumns}`}; } - if (aggregateFieldCount > 1) { - // Only aggregates: 1fr auto ... auto - return {gridTemplateColumns: `1fr repeat(${aggregateFieldCount - 1}, auto)`}; - } // Single column or only groupBys return { gridTemplateColumns: - fields.length > 1 ? `repeat(${fields.length - 1}, auto) 1fr` : '1fr', + displayFields.length > 1 + ? `repeat(${displayFields.length - 1}, auto) 1fr` + : '1fr', }; - }, [aggregateFieldCount, fields.length, groupByFieldCount]); + }, [aggregateFieldCount, displayFields.length, groupByFieldCount]); const firstColumnOffset = useMemo(() => { return groupBys.length > 0 ? '15px' : '8px'; }, [groupBys]); const isLastColumn = (index: number) => { - return index === fields.length - 1; + return index === displayFields.length - 1; }; // Dividers: between last groupBy and first aggregate, and between all aggregates @@ -124,8 +153,8 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab return true; } - // Between aggregate columns (not the last one), we use fields here because we need the total number of columns - if (index > groupByFieldCount && index < fields.length) { + // Between aggregate columns (not the last one) + if (index > groupByFieldCount && index < displayFields.length) { return true; } @@ -193,12 +222,14 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab {isPending && } - {fields.map((field, i) => { + {displayFields.map((field, i) => { let label = field; const tag = stringTags?.[field] ?? numberTags?.[field] ?? booleanTags?.[field] ?? null; const func = parseFunction(field); - if (func) { + if (field === TraceMetricKnownFieldKey.METRIC_NAME) { + label = t('Metric'); + } else if (func) { label = `${func.name}(…)`; } else if (tag) { label = tag.name; @@ -207,6 +238,7 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab } const direction = sorts.find(s => s.field === field)?.kind; + const canSort = displayColumns[i]?.isSortable !== false; function updateSort() { const kind = direction === 'desc' ? 'asc' : 'desc'; @@ -221,7 +253,7 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab isAggregate={Boolean(func)} isSticky={isLastColumn(i)} sort={direction} - handleSortClick={updateSort} + handleSortClick={canSort ? updateSort : undefined} > {label} @@ -237,30 +269,36 @@ export function AggregatesTab({traceMetric, isMetricOptionsEmpty}: AggregatesTab ) : result.data?.length ? ( - result.data.map((row, i) => ( - - {topEvents && i < topEvents && ( - - )} - {fields.map((field, j) => ( - - - - ))} - - )) + result.data.map((row, i) => { + const displayRow = + groupBys.length === 0 + ? {...row, [TraceMetricKnownFieldKey.METRIC_NAME]: traceMetric.name} + : row; + return ( + + {topEvents && i < topEvents && ( + + )} + {displayFields.map((field, j) => ( + + + + ))} + + ); + }) ) : isPending ? ( From 1a4b4cc8138d1bb9923225b2f6952d7f384ccf59 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 30 Mar 2026 18:11:37 +0200 Subject: [PATCH 05/57] fix(preprod): Prefer display_name for snapshot sidebar labels (#111779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snapshot sidebar label fallback chain was `group → image_file_name`, which meant `display_name` was never shown even when set. This updates all three label sites to prefer `display_name` first: `display_name → group → image_file_name`. Grouping logic (`getImageGroup`) is unchanged — only the displayed label is affected. Before: Screenshot 2026-03-30 at 11 09 02 After: Screenshot 2026-03-30 at 10 59 49 --------- Co-authored-by: Claude Opus 4.6 --- static/app/views/preprod/snapshots/snapshots.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/app/views/preprod/snapshots/snapshots.tsx b/static/app/views/preprod/snapshots/snapshots.tsx index e0945ac8ce14..5be68a236efa 100644 --- a/static/app/views/preprod/snapshots/snapshots.tsx +++ b/static/app/views/preprod/snapshots/snapshots.tsx @@ -130,6 +130,7 @@ export default function SnapshotsPage() { for (const [groupKey, groupedPairs] of groups) { const label = groupedPairs[0]!.head_image.group ?? + groupedPairs[0]!.head_image.display_name ?? groupedPairs[0]!.head_image.image_file_name; items.push({ type, @@ -156,7 +157,8 @@ export default function SnapshotsPage() { } } for (const [groupKey, images] of groups) { - const label = images[0]!.group ?? images[0]!.image_file_name; + const label = + images[0]!.group ?? images[0]!.display_name ?? images[0]!.image_file_name; items.push({ type, key: `${type}:${groupKey}`, @@ -195,7 +197,8 @@ export default function SnapshotsPage() { return [...groups.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([groupKey, images]) => { - const label = images[0]!.group ?? images[0]!.image_file_name; + const label = + images[0]!.group ?? images[0]!.display_name ?? images[0]!.image_file_name; return { type: 'solo' as const, key: `solo:${groupKey}`, From a154d99053f9d1052dad53e2c197d865807cc14b Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 30 Mar 2026 12:12:21 -0400 Subject: [PATCH 06/57] ref(metrics): Tidy up metrics UI for refresh layout (#111532) Update the metrics explore tab when the UI refresh flag is enabled: - Replace the eye icon toggle in the metric toolbar with a letter label that swaps to a closed eye icon when hidden, matching the pattern used in the spans tab - Remove the Samples and Aggregates tabs from the metric panel when hidden --------- Co-authored-by: Claude Opus 4.6 --- .../explore/metrics/metricPanel/index.tsx | 19 +++++++----- .../metrics/metricToolbar/visualizeLabel.tsx | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/static/app/views/explore/metrics/metricPanel/index.tsx b/static/app/views/explore/metrics/metricPanel/index.tsx index 62ccad8ce367..78873fe9ed95 100644 --- a/static/app/views/explore/metrics/metricPanel/index.tsx +++ b/static/app/views/explore/metrics/metricPanel/index.tsx @@ -20,6 +20,7 @@ import {SideBySideOrientation} from 'sentry/views/explore/metrics/metricPanel/si import {StackedOrientation} from 'sentry/views/explore/metrics/metricPanel/stackedOrientation'; import {type TraceMetric} from 'sentry/views/explore/metrics/metricQuery'; import {canUseMetricsUIRefresh} from 'sentry/views/explore/metrics/metricsFlags'; +import {useMetricVisualize} from 'sentry/views/explore/metrics/metricsQueryParams'; import {getMetricTableColumnType} from 'sentry/views/explore/metrics/utils'; import { useQueryParamsAggregateSortBys, @@ -87,6 +88,8 @@ export function MetricPanel({traceMetric, queryIndex}: MetricPanelProps) { panelIndex: queryIndex, }); + const visualize = useMetricVisualize(); + if (hasMetricsUIRefresh) { return ( @@ -98,13 +101,15 @@ export function MetricPanel({traceMetric, queryIndex}: MetricPanelProps) { isMetricOptionsEmpty={isMetricOptionsEmpty} queryIndex={queryIndex} /> - + {visualize.visible && ( + + )} diff --git a/static/app/views/explore/metrics/metricToolbar/visualizeLabel.tsx b/static/app/views/explore/metrics/metricToolbar/visualizeLabel.tsx index 5906dd29d203..6ed730d01c1a 100644 --- a/static/app/views/explore/metrics/metricToolbar/visualizeLabel.tsx +++ b/static/app/views/explore/metrics/metricToolbar/visualizeLabel.tsx @@ -6,6 +6,8 @@ import {Text} from '@sentry/scraps/text'; import {IconShow} from 'sentry/icons'; import {IconHide} from 'sentry/icons/iconHide'; +import {useOrganization} from 'sentry/utils/useOrganization'; +import {canUseMetricsUIRefresh} from 'sentry/views/explore/metrics/metricsFlags'; import type {Visualize} from 'sentry/views/explore/queryParams/visualize'; import {getVisualizeLabel} from 'sentry/views/explore/toolbar/toolbarVisualize'; @@ -16,6 +18,24 @@ interface VisualizeLabelProps { } export function VisualizeLabel({index, onClick, visualize}: VisualizeLabelProps) { + const organization = useOrganization(); + + if (canUseMetricsUIRefresh(organization)) { + const label = visualize.visible ? ( + + {getVisualizeLabel(index)} + + ) : ( + + ); + + return ( + + {label} + + ); + } + const label = getVisualizeLabel(index); const icon = visualize.visible ? : ; @@ -31,6 +51,15 @@ export function VisualizeLabel({index, onClick, visualize}: VisualizeLabelProps) ); } +const RefreshLabel = styled(Flex)` + cursor: pointer; + background-color: ${p => p.theme.tokens.background.transparent.accent.muted}; + color: ${p => p.theme.tokens.content.accent}; + width: 24px; + height: 36px; + border-radius: ${p => p.theme.radius.md}; +`; + const IconLabel = styled(Flex)` cursor: pointer; font-weight: bold; From dd2f2eca68557591e4f32e6768e76565bc24f390 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:19:03 -0400 Subject: [PATCH 07/57] chore(dashboards): Add consistent settings files to all prebuilt dashboard configs (#111788) ## Summary - Add `settings.ts` files to `ai/`, `mobileVitals/`, and `webVitals/` prebuilt dashboard config folders that were missing them - Consolidate `queries/constants.ts` into `queries/settings.ts` so titles and constants live in one place - All dashboard titles are now centralized in settings files with proper `t()` i18n wrapping (previously AI and webVitals titles were plain strings) - Updated 15 config files to import their dashboard titles from the new settings files ## Test plan - [ ] Verify prebuilt dashboards still render correctly with their expected titles - [ ] Confirm no regressions in dashboard creation flow --------- Co-authored-by: Claude Opus 4.6 --- .../views/dashboards/utils/prebuiltConfigs.tsx | 16 ++++++++-------- .../utils/prebuiltConfigs/ai/aiAgentsModels.ts | 3 ++- .../utils/prebuiltConfigs/ai/aiAgentsOverview.ts | 3 ++- .../utils/prebuiltConfigs/ai/aiAgentsTools.ts | 3 ++- .../utils/prebuiltConfigs/ai/mcpOverview.ts | 3 ++- .../utils/prebuiltConfigs/ai/mcpPrompts.ts | 3 ++- .../utils/prebuiltConfigs/ai/mcpResources.ts | 3 ++- .../utils/prebuiltConfigs/ai/mcpTools.ts | 3 ++- .../utils/prebuiltConfigs/ai/settings.ts | 9 +++++++++ ...AssetsSummary.ts => frontendAssetsDetails.ts} | 6 +++--- .../prebuiltConfigs/frontendAssets/settings.ts | 2 +- .../prebuiltConfigs/mobileVitals/appStarts.ts | 3 ++- .../prebuiltConfigs/mobileVitals/mobileVitals.ts | 3 ++- .../prebuiltConfigs/mobileVitals/screenLoads.ts | 3 ++- .../mobileVitals/screenRendering.ts | 3 ++- .../prebuiltConfigs/mobileVitals/settings.ts | 6 ++++++ .../utils/prebuiltConfigs/queries/constants.ts | 4 ---- .../utils/prebuiltConfigs/queries/queries.ts | 7 ++++--- .../queries/{querySummary.ts => queryDetails.ts} | 9 +++++---- .../utils/prebuiltConfigs/queries/settings.ts | 7 +++++++ .../queues/{queueSummary.ts => queueDetails.ts} | 6 +++--- .../utils/prebuiltConfigs/queues/settings.ts | 2 +- .../webVitals/{pageSummary.ts => pageDetails.ts} | 5 +++-- .../utils/prebuiltConfigs/webVitals/settings.ts | 4 ++++ .../utils/prebuiltConfigs/webVitals/webVitals.ts | 3 ++- 25 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/ai/settings.ts rename static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/{frontendAssetsSummary.ts => frontendAssetsDetails.ts} (98%) create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings.ts delete mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/queries/constants.ts rename static/app/views/dashboards/utils/prebuiltConfigs/queries/{querySummary.ts => queryDetails.ts} (95%) rename static/app/views/dashboards/utils/prebuiltConfigs/queues/{queueSummary.ts => queueDetails.ts} (97%) rename static/app/views/dashboards/utils/prebuiltConfigs/webVitals/{pageSummary.ts => pageDetails.ts} (98%) create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/webVitals/settings.ts diff --git a/static/app/views/dashboards/utils/prebuiltConfigs.tsx b/static/app/views/dashboards/utils/prebuiltConfigs.tsx index 2d4fc33a70bd..03453b799f9d 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs.tsx +++ b/static/app/views/dashboards/utils/prebuiltConfigs.tsx @@ -10,7 +10,7 @@ import {MCP_TOOLS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltC import {BACKEND_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview'; import {CACHES_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/caches/caches'; import {FRONTEND_ASSETS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssets'; -import {FRONTEND_ASSETS_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsSummary'; +import {FRONTEND_ASSETS_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails'; import {FRONTEND_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendOverview/frontendOverview'; import {HTTP_DOMAIN_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/http/domainSummary'; import {HTTP_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/http/http'; @@ -22,11 +22,11 @@ import {MOBILE_VITALS_SCREEN_LOADS_PREBUILT_CONFIG} from 'sentry/views/dashboard import {MOBILE_VITALS_SCREEN_RENDERING_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering'; import {NEXTJS_FRONTEND_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview'; import {QUERIES_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/queries'; -import {QUERIES_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/querySummary'; +import {QUERIES_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/queryDetails'; +import {QUEUE_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueDetails'; import {QUEUES_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queues'; -import {QUEUE_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueSummary'; import {SESSION_HEALTH_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/sessionHealth'; -import {WEB_VITALS_SUMMARY_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/pageSummary'; +import {WEB_VITALS_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/pageDetails'; import {WEB_VITALS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals'; import type {ModulesWithOnboarding} from 'sentry/views/insights/common/components/modulesOnboarding'; @@ -97,11 +97,11 @@ export type PrebuiltDashboard = Omit & { export const PREBUILT_DASHBOARDS: Record = { [PrebuiltDashboardId.FRONTEND_SESSION_HEALTH]: SESSION_HEALTH_PREBUILT_CONFIG, [PrebuiltDashboardId.BACKEND_QUERIES]: QUERIES_PREBUILT_CONFIG, - [PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY]: QUERIES_SUMMARY_PREBUILT_CONFIG, + [PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY]: QUERIES_DETAILS_PREBUILT_CONFIG, [PrebuiltDashboardId.HTTP]: HTTP_PREBUILT_CONFIG, [PrebuiltDashboardId.HTTP_DOMAIN_SUMMARY]: HTTP_DOMAIN_SUMMARY_PREBUILT_CONFIG, [PrebuiltDashboardId.WEB_VITALS]: WEB_VITALS_PREBUILT_CONFIG, - [PrebuiltDashboardId.WEB_VITALS_SUMMARY]: WEB_VITALS_SUMMARY_PREBUILT_CONFIG, + [PrebuiltDashboardId.WEB_VITALS_SUMMARY]: WEB_VITALS_DETAILS_PREBUILT_CONFIG, [PrebuiltDashboardId.MOBILE_VITALS]: MOBILE_VITALS_PREBUILT_CONFIG, [PrebuiltDashboardId.BACKEND_OVERVIEW]: BACKEND_OVERVIEW_PREBUILT_CONFIG, [PrebuiltDashboardId.MOBILE_VITALS_APP_STARTS]: @@ -123,8 +123,8 @@ export const PREBUILT_DASHBOARDS: Record [PrebuiltDashboardId.MCP_OVERVIEW]: MCP_OVERVIEW_PREBUILT_CONFIG, [PrebuiltDashboardId.LARAVEL_OVERVIEW]: LARAVEL_OVERVIEW_PREBUILT_CONFIG, [PrebuiltDashboardId.FRONTEND_ASSETS]: FRONTEND_ASSETS_PREBUILT_CONFIG, - [PrebuiltDashboardId.FRONTEND_ASSETS_SUMMARY]: FRONTEND_ASSETS_SUMMARY_PREBUILT_CONFIG, + [PrebuiltDashboardId.FRONTEND_ASSETS_SUMMARY]: FRONTEND_ASSETS_DETAILS_PREBUILT_CONFIG, [PrebuiltDashboardId.BACKEND_QUEUES]: QUEUES_PREBUILT_CONFIG, - [PrebuiltDashboardId.BACKEND_QUEUE_SUMMARY]: QUEUE_SUMMARY_PREBUILT_CONFIG, + [PrebuiltDashboardId.BACKEND_QUEUE_SUMMARY]: QUEUE_DETAILS_PREBUILT_CONFIG, [PrebuiltDashboardId.BACKEND_CACHES]: CACHES_PREBUILT_CONFIG, }; diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts index 93cef174c639..4902f0364aec 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsModels.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {AI_AGENTS_MODELS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields} from 'sentry/views/insights/types'; @@ -157,7 +158,7 @@ const MODELS_TABLE = { export const AI_AGENTS_MODELS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'AI Agents Model Details', + title: AI_AGENTS_MODELS_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts index 7fe33fd5c34d..52465ac2e18b 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview.ts @@ -3,6 +3,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, MAX_TABLE_LIMIT, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {AI_AGENTS_OVERVIEW_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import { WIDGET_COLUMN_LABELS, TABLE_MIN_HEIGHT, @@ -225,7 +226,7 @@ const AGENTS_TRACES_TABLE = { export const AI_AGENTS_OVERVIEW_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'AI Agents Overview', + title: AI_AGENTS_OVERVIEW_DASHBOARD_TITLE, filters: { globalFilter: DEFAULT_GLOBAL_FILTERS, }, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts index d227643b4300..acc1cfd9ad15 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsTools.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {AI_AGENTS_TOOLS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields} from 'sentry/views/insights/types'; @@ -101,7 +102,7 @@ const TOOLS_TABLE = { export const AI_AGENTS_TOOLS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'AI Agents Tool Details', + title: AI_AGENTS_TOOLS_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts index 685ea6e019dd..4402cbdc225d 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpOverview.ts @@ -1,6 +1,7 @@ import {t} from 'sentry/locale'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {MCP_OVERVIEW_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields, SpanFunction} from 'sentry/views/insights/types'; @@ -212,7 +213,7 @@ const OVERVIEW_TABLE = { export const MCP_OVERVIEW_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'MCP Overview', + title: MCP_OVERVIEW_DASHBOARD_TITLE, filters: {}, widgets: [...FIRST_ROW_WIDGETS, ...SECOND_ROW_WIDGETS, OVERVIEW_TABLE], onboarding: { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts index fc83f17e34d0..914f705a9440 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpPrompts.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {MCP_PROMPTS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields, SpanFunction} from 'sentry/views/insights/types'; @@ -124,7 +125,7 @@ const PROMPTS_TABLE = { export const MCP_PROMPTS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'MCP Prompt Details', + title: MCP_PROMPTS_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts index 70086c286683..363a577582af 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpResources.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {MCP_RESOURCES_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields, SpanFunction} from 'sentry/views/insights/types'; @@ -124,7 +125,7 @@ const RESOURCES_TABLE = { export const MCP_RESOURCES_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'MCP Resource Details', + title: MCP_RESOURCES_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts index fb1fe5c63d7d..cf49305811d1 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/mcpTools.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {MCP_TOOLS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {SpanFields, SpanFunction} from 'sentry/views/insights/types'; @@ -124,7 +125,7 @@ const TOOLS_TABLE = { export const MCP_TOOLS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'MCP Tool Details', + title: MCP_TOOLS_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/ai/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/ai/settings.ts new file mode 100644 index 000000000000..282e2d548d90 --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/ai/settings.ts @@ -0,0 +1,9 @@ +import {t} from 'sentry/locale'; + +export const AI_AGENTS_OVERVIEW_DASHBOARD_TITLE = t('AI Agents Overview'); +export const AI_AGENTS_MODELS_DASHBOARD_TITLE = t('AI Agents Model Details'); +export const AI_AGENTS_TOOLS_DASHBOARD_TITLE = t('AI Agents Tool Details'); +export const MCP_OVERVIEW_DASHBOARD_TITLE = t('MCP Overview'); +export const MCP_TOOLS_DASHBOARD_TITLE = t('MCP Tool Details'); +export const MCP_PROMPTS_DASHBOARD_TITLE = t('MCP Prompt Details'); +export const MCP_RESOURCES_DASHBOARD_TITLE = t('MCP Resource Details'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsSummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts similarity index 98% rename from static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsSummary.ts rename to static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts index 03a743b2de0f..9272c872298c 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsSummary.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/frontendAssetsDetails.ts @@ -2,7 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; -import {SUMMARY_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings'; +import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import type {DefaultDetailWidgetFields} from 'sentry/views/dashboards/widgets/detailsWidget/types'; @@ -271,7 +271,7 @@ const ASSETS_TABLE_WIDGET: Widget = { }, }; -export const FRONTEND_ASSETS_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = { +export const FRONTEND_ASSETS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], filters: { @@ -296,7 +296,7 @@ export const FRONTEND_ASSETS_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = { }, ], }, - title: SUMMARY_DASHBOARD_TITLE, + title: DETAILS_DASHBOARD_TITLE, widgets: [ ASSET_DESCRIPTION_WIDGET, ...SECOND_ROW_WIDGETS, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings.ts index dfbc6c35ac81..9c5032d67a19 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/frontendAssets/settings.ts @@ -1,4 +1,4 @@ import {t} from 'sentry/locale'; export const DASHBOARD_TITLE = t('Frontend Assets'); -export const SUMMARY_DASHBOARD_TITLE = t('Frontend Assets Summary'); +export const DETAILS_DASHBOARD_TITLE = t('Frontend Assets Summary'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts index 591c189eca67..75cb62e4a1de 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/appStarts.ts @@ -3,6 +3,7 @@ import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {APP_STARTS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; @@ -315,7 +316,7 @@ const SECOND_ROW_WIDGETS: Widget[] = [ export const MOBILE_VITALS_APP_STARTS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', - title: t('Mobile Vitals App Starts'), + title: APP_STARTS_DASHBOARD_TITLE, projects: [], widgets: [ ...HEADER_ROW_WIDGETS, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts index 8c3d1ff9f08f..e32f42f97ec8 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/mobileVitals.ts @@ -3,6 +3,7 @@ import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings'; import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; @@ -426,7 +427,7 @@ const SECOND_ROW_WIDGETS: Widget[] = [ export const MOBILE_VITALS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', - title: t('Mobile Vitals'), + title: DASHBOARD_TITLE, projects: [], widgets: [ ...FIRST_ROW_WIDGETS, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts index a2ba003fd7b8..24a1f644a6d1 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads.ts @@ -3,6 +3,7 @@ import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {SCREEN_LOADS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; const TRANSACTION_CONDITION = `is_transaction:true ${SpanFields.TRANSACTION_OP}:[ui.load,navigation]`; @@ -298,7 +299,7 @@ const THIRD_ROW_WIDGETS: Widget[] = [TTID_BAR_CHART_WIDGET, TTFD_BAR_CHART_WIDGE export const MOBILE_VITALS_SCREEN_LOADS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', - title: t('Mobile Vitals Screen Loads'), + title: SCREEN_LOADS_DASHBOARD_TITLE, projects: [], widgets: [ ...HEADER_ROW_WIDGETS, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts index 610b2678bf93..35ea5e0439a7 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering.ts @@ -3,6 +3,7 @@ import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {SCREEN_RENDERING_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; const SPAN_OPERATIONS_CONDITION = `${SpanFields.SPAN_OP}:[app.start.cold,app.start.warm,contentprovider.load,application.load,activity.load,ui.load,process.load]`; @@ -52,7 +53,7 @@ const SPAN_OPERATIONS_TABLE: Widget = { export const MOBILE_VITALS_SCREEN_RENDERING_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', - title: t('Mobile Vitals Screen Rendering'), + title: SCREEN_RENDERING_DASHBOARD_TITLE, projects: [], widgets: [SPAN_OPERATIONS_TABLE], filters: { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings.ts new file mode 100644 index 000000000000..145472978b75 --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/mobileVitals/settings.ts @@ -0,0 +1,6 @@ +import {t} from 'sentry/locale'; + +export const DASHBOARD_TITLE = t('Mobile Vitals'); +export const APP_STARTS_DASHBOARD_TITLE = t('Mobile Vitals App Starts'); +export const SCREEN_LOADS_DASHBOARD_TITLE = t('Mobile Vitals Screen Loads'); +export const SCREEN_RENDERING_DASHBOARD_TITLE = t('Mobile Vitals Screen Rendering'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/constants.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/constants.ts deleted file mode 100644 index 9c00f2654a25..000000000000 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {t} from 'sentry/locale'; - -export const QUERIES_PER_MINUTE_TEXT = t('Queries Per Minute'); -export const AVERAGE_DURATION_TEXT = t('Average Duration'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts index f1a34d35e0c7..1763bde6a283 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts @@ -5,16 +5,17 @@ import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; import { AVERAGE_DURATION_TEXT, + BASE_FILTER_STRING, + DASHBOARD_TITLE, QUERIES_PER_MINUTE_TEXT, -} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/constants'; -import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings'; +} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings'; import {DataTitles} from 'sentry/views/insights/common/views/spans/types'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; export const QUERIES_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'Queries', + title: DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queryDetails.ts similarity index 95% rename from static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts rename to static/app/views/dashboards/utils/prebuiltConfigs/queries/queryDetails.ts index 2a6dab0c83e5..0788200b1d67 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queryDetails.ts @@ -4,16 +4,17 @@ import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; import { AVERAGE_DURATION_TEXT, + BASE_FILTER_STRING, QUERIES_PER_MINUTE_TEXT, -} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/constants'; -import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings'; + DETAILS_DASHBOARD_TITLE, +} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings'; import {WIDGET_COLUMN_LABELS} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; -export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = { +export const QUERIES_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'Query Details', + title: DETAILS_DASHBOARD_TITLE, filters: { globalFilter: [ { diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts index ab2adbd97636..629d0ef8b314 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts @@ -1,6 +1,13 @@ +import {t} from 'sentry/locale'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; +export const DASHBOARD_TITLE = t('Queries'); +export const DETAILS_DASHBOARD_TITLE = t('Query Details'); + +export const QUERIES_PER_MINUTE_TEXT = t('Queries Per Minute'); +export const AVERAGE_DURATION_TEXT = t('Average Duration'); + const BASE_FILTERS = { [SpanFields.SPAN_CATEGORY]: ModuleName.DB, has: SpanFields.NORMALIZED_DESCRIPTION, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueSummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts similarity index 97% rename from static/app/views/dashboards/utils/prebuiltConfigs/queues/queueSummary.ts rename to static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts index 8639b2930ff6..4a64a03f9b3a 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueSummary.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/queues/queueDetails.ts @@ -2,7 +2,7 @@ import {t} from 'sentry/locale'; import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types'; import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; import {QUEUE_CHARTS} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueCharts'; -import {SUMMARY_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/settings'; +import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/settings'; import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; @@ -208,10 +208,10 @@ const CONSUMER_TABLE: Widget = { }, }; -export const QUEUE_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = { +export const QUEUE_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: SUMMARY_DASHBOARD_TITLE, + title: DETAILS_DASHBOARD_TITLE, filters: {}, widgets: [...FIRST_ROW_WIDGTS, ...SECOND_ROW_WIDGETS, CONSUMER_TABLE, PRODUCER_TABLE], onboarding: {type: 'module', moduleName: ModuleName.QUEUE}, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queues/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queues/settings.ts index ec13df0d68cf..fae8dbf1489c 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/queues/settings.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/queues/settings.ts @@ -1,4 +1,4 @@ import {t} from 'sentry/locale'; export const DASHBOARD_TITLE = t('Queues'); -export const SUMMARY_DASHBOARD_TITLE = t('Queue Summary'); +export const DETAILS_DASHBOARD_TITLE = t('Queue Summary'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageSummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageDetails.ts similarity index 98% rename from static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageSummary.ts rename to static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageDetails.ts index 89b205620d03..58b81b95f49a 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageSummary.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/pageDetails.ts @@ -1,14 +1,15 @@ import {t} from 'sentry/locale'; import {DisplayType, SlideoutId, WidgetType} from 'sentry/views/dashboards/types'; import {type PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {DETAILS_DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/settings'; import {ISSUE_TYPES} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals'; import {DEFAULT_QUERY_FILTER} from 'sentry/views/insights/browser/webVitals/settings'; import {ModuleName} from 'sentry/views/insights/types'; -export const WEB_VITALS_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = { +export const WEB_VITALS_DETAILS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'Web Vitals Page Summary', + title: DETAILS_DASHBOARD_TITLE, filters: { globalFilter: [], }, diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/settings.ts new file mode 100644 index 000000000000..d9aded3db39d --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/settings.ts @@ -0,0 +1,4 @@ +import {t} from 'sentry/locale'; + +export const DASHBOARD_TITLE = t('Web Vitals'); +export const DETAILS_DASHBOARD_TITLE = t('Web Vitals Page Summary'); diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals.ts b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals.ts index f85210c15dbe..ca0d2b2a0b2d 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/webVitals/webVitals.ts @@ -2,6 +2,7 @@ import {t} from 'sentry/locale'; import {FieldKind} from 'sentry/utils/fields'; import {DisplayType, SlideoutId, WidgetType} from 'sentry/views/dashboards/types'; import {type PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/webVitals/settings'; import {SCORE_BREAKDOWN_WHEEL_WIDGET} from 'sentry/views/dashboards/widgetLibrary/webVitalsWidgets'; import {DEFAULT_QUERY_FILTER} from 'sentry/views/insights/browser/webVitals/settings'; import {ModuleName, SpanFields} from 'sentry/views/insights/types'; @@ -20,7 +21,7 @@ export const ISSUE_TYPES = [ export const WEB_VITALS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', projects: [], - title: 'Web Vitals', + title: DASHBOARD_TITLE, filters: { globalFilter: [ { From c2a682c0a6f76f9e7b34a99b1a8073186b45ac06 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:26:33 -0400 Subject: [PATCH 08/57] feat(dashboards): Adds `dashboards-ai-generate` flag to Seer Explorer access list (#111667) Seer Explorer chat endpoints are gated by a list of flags and passes if at least one is fulfilled. Adds `dashboards-ai-generate` to the list of flags. --- .../organization_seer_explorer_chat.py | 21 +++++- .../test_organization_seer_explorer_chat.py | 74 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/sentry/seer/endpoints/organization_seer_explorer_chat.py b/src/sentry/seer/endpoints/organization_seer_explorer_chat.py index f07d39ed0a7b..3969a125af1f 100644 --- a/src/sentry/seer/endpoints/organization_seer_explorer_chat.py +++ b/src/sentry/seer/endpoints/organization_seer_explorer_chat.py @@ -17,6 +17,7 @@ from sentry.seer.explorer.client import SeerExplorerClient from sentry.seer.explorer.client_utils import has_seer_explorer_access_with_detail from sentry.seer.models import SeerPermissionError +from sentry.seer.seer_setup import has_seer_access_with_detail from sentry.types.ratelimit import RateLimit, RateLimitCategory logger = logging.getLogger(__name__) @@ -83,7 +84,13 @@ def get( Get the current state of a Seer Explorer session. """ has_access, error = has_seer_explorer_access_with_detail(organization, request.user) - if not has_access: + + has_seer_access, _ = has_seer_access_with_detail(organization, request.user) + has_dashboards_ai_generate_access = has_seer_access and features.has( + "organizations:dashboards-ai-generate", organization, actor=request.user + ) + + if not has_access and not has_dashboards_ai_generate_access: raise PermissionDenied(error) if not run_id: @@ -115,7 +122,17 @@ def post( - run_id: The run ID. """ has_access, error = has_seer_explorer_access_with_detail(organization, request.user) - if not has_access: + + has_seer_access, _ = has_seer_access_with_detail(organization, request.user) + has_dashboards_ai_generate_access = has_seer_access and features.has( + "organizations:dashboards-ai-generate", organization, actor=request.user + ) + # Orgs with dashboards AI generate access can continue existing dashboard generate runs, but cannot start new runs from this endpoint. + can_continue_dashboards_generate_run = ( + has_dashboards_ai_generate_access and run_id is not None + ) + + if not has_access and not can_continue_dashboards_generate_run: raise PermissionDenied(error) serializer = SeerExplorerChatSerializer(data=request.data) diff --git a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py index 98a193278d3b..22497f3ba2cc 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py @@ -151,6 +151,80 @@ def test_post_continue_conversation_enable_coding(self, mock_client_class: Magic enable_coding=feature_enabled and option_enabled, ) + @patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient") + def test_get_run_allowed_with_dashboards_ai_generate_flag( + self, mock_client_class: MagicMock + ) -> None: + """GET with run_id should succeed with dashboards-ai-generate flag even without seer-explorer.""" + from sentry.seer.explorer.client_models import SeerRunState + + mock_state = SeerRunState( + run_id=123, + blocks=[], + status="completed", + updated_at="2024-01-01T00:00:00Z", + ) + mock_client = MagicMock() + mock_client.get_run.return_value = mock_state + mock_client_class.return_value = mock_client + + with self.feature( + { + "organizations:seer-explorer": False, + "organizations:dashboards-ai-generate": True, + } + ): + response = self.client.get(f"{self.url}123/") + + assert response.status_code == 200 + assert response.data["session"]["run_id"] == 123 + + @patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient") + def test_continue_run_allowed_with_dashboards_ai_generate_flag( + self, mock_client_class: MagicMock + ) -> None: + """POST with run_id should succeed with dashboards-ai-generate flag.""" + mock_client = MagicMock() + mock_client.continue_run.return_value = 789 + mock_client_class.return_value = mock_client + + data = {"query": "Follow up question"} + with self.feature( + { + "organizations:seer-explorer": False, + "organizations:dashboards-ai-generate": True, + } + ): + response = self.client.post(f"{self.url}789/", data, format="json") + + assert response.status_code == 200 + assert response.data == {"run_id": 789} + + def test_new_run_denied_without_seer_explorer_flag(self) -> None: + """POST without run_id should be denied with only dashboards-ai-generate flag.""" + data = {"query": "Start a new conversation"} + with self.feature( + { + "organizations:seer-explorer": False, + "organizations:dashboards-ai-generate": True, + } + ): + response = self.client.post(self.url, data, format="json") + + assert response.status_code == 403 + + def test_get_denied_without_either_flag(self) -> None: + """GET should be denied without seer-explorer or dashboards-ai-generate.""" + with self.feature( + { + "organizations:seer-explorer": False, + "organizations:dashboards-ai-generate": False, + } + ): + response = self.client.get(self.url) + + assert response.status_code == 403 + @with_feature("organizations:seer-explorer") @with_feature("organizations:gen-ai-features") From bfa1b15dde74f5b61e38c385146aa9bab0874a1b Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 30 Mar 2026 09:31:45 -0700 Subject: [PATCH 09/57] =?UTF-8?q?feat(metric-issues):=20Use=20placeholder?= =?UTF-8?q?=20loaders=20for=20attribute=20comparison=E2=80=A6=20(#111676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tiny update, renders a placeholder for each chart instead of one big one which doesn't always match the correct size. --- .../streamline/sidebar/attributeComparisonSection.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/streamline/sidebar/attributeComparisonSection.tsx b/static/app/views/issueDetails/streamline/sidebar/attributeComparisonSection.tsx index 9c4332ab8c41..86d77a42d845 100644 --- a/static/app/views/issueDetails/streamline/sidebar/attributeComparisonSection.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/attributeComparisonSection.tsx @@ -149,7 +149,11 @@ export function AttributeComparisonSection({ {isLoading ? ( - + + {Array.from({length: CHARTS_PER_PAGE}).map((_, i) => ( + + ))} + ) : error ? ( ) : filteredRankedAttributes.length > 0 ? ( From eb411f897717f70868bb6f0362678f10a7ffb13d Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Mon, 30 Mar 2026 09:35:17 -0700 Subject: [PATCH 10/57] perf(preprod): Parallelize and deduplicate snapshot image fetches (#111755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces sequential `session.get()` calls in `compare_snapshots` with a `ContextPropagatingThreadPoolExecutor` (8 workers) that prefetches all unique content hashes for each pixel batch concurrently. A per-batch `dict` cache keyed by content hash deduplicates fetches — if the same blob appears as `head_hash` for one pair and `base_hash` for another, it's only downloaded once. Previously, each image pair fetched its head and base images sequentially, with no dedup across pairs. For a batch of 50 pairs sharing some hashes, this could mean 100 individual blocking HTTP round-trips when only ~70 unique hashes exist. The objectstore `Session.get()` is thread-safe (no mutable instance state, urllib3 pool handles concurrency internally), so no changes to the objectstore client are needed. Closes EME-828 --------- Co-authored-by: Claude Opus 4.6 --- src/sentry/preprod/snapshots/tasks.py | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/sentry/preprod/snapshots/tasks.py b/src/sentry/preprod/snapshots/tasks.py index 32107e22af75..5ce11f0f8b4a 100644 --- a/src/sentry/preprod/snapshots/tasks.py +++ b/src/sentry/preprod/snapshots/tasks.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import threading from typing import NamedTuple import orjson @@ -27,6 +28,7 @@ from sentry.tasks.base import instrumented_task from sentry.taskworker.namespaces import preprod_tasks from sentry.utils import metrics +from sentry.utils.concurrent import ContextPropagatingThreadPoolExecutor logger = logging.getLogger(__name__) @@ -353,15 +355,30 @@ def compare_snapshots( batch_names: list[str] = [] batch_hashes: list[tuple[str, str]] = [] + unique_hashes: set[str] = set() for candidate in batch: + unique_hashes.add(candidate.head_hash) + unique_hashes.add(candidate.base_hash) + + fetch_cache: dict[str, bytes] = {} + failed_hashes: set[str] = set() + cache_lock = threading.Lock() + + def _fetch_hash(h: str) -> None: try: - head_data = session.get( - f"{image_key_prefix}/{candidate.head_hash}" - ).payload.read() - base_data = session.get( - f"{image_key_prefix}/{candidate.base_hash}" - ).payload.read() + data = session.get(f"{image_key_prefix}/{h}").payload.read() + with cache_lock: + fetch_cache[h] = data except Exception: + with cache_lock: + failed_hashes.add(h) + + # Fetch unique hashes in parallel; session.get() is thread-safe + with ContextPropagatingThreadPoolExecutor(max_workers=8) as executor: + list(executor.map(_fetch_hash, unique_hashes)) + + for candidate in batch: + if candidate.head_hash in failed_hashes or candidate.base_hash in failed_hashes: logger.warning( "compare_snapshots: failed to fetch images for %s", candidate.name, @@ -379,6 +396,8 @@ def compare_snapshots( "reason": "image_fetch_failed", } continue + head_data = fetch_cache[candidate.head_hash] + base_data = fetch_cache[candidate.base_hash] total_fetched_bytes += len(head_data) + len(base_data) total_fetched_count += 2 diff_pairs.append((base_data, head_data)) @@ -386,8 +405,9 @@ def compare_snapshots( batch_hashes.append((candidate.head_hash, candidate.base_hash)) logger.info( - "compare_snapshots: running batch of %d pairs", + "compare_snapshots: running batch of %d pairs (%d unique hashes fetched)", len(diff_pairs), + len(fetch_cache), extra={"head_artifact_id": head_artifact_id, "names": batch_names}, ) diff_results = compare_images_batch(diff_pairs, server=server) From 01f96266a6cb19e6b5064c8fff6e3b2a485a2328 Mon Sep 17 00:00:00 2001 From: Gabe Villalobos Date: Mon, 30 Mar 2026 09:44:19 -0700 Subject: [PATCH 11/57] chore(eco): Adds a new organization create CLI command (#111765) Adds a CLI command for quickly creating new organizations with a desired slug and owning user --- src/sentry/runner/commands/createorg.py | 58 +++++++++++++++++++ src/sentry/runner/main.py | 1 + .../sentry/runner/commands/test_createorg.py | 47 +++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/sentry/runner/commands/createorg.py create mode 100644 tests/sentry/runner/commands/test_createorg.py diff --git a/src/sentry/runner/commands/createorg.py b/src/sentry/runner/commands/createorg.py new file mode 100644 index 000000000000..67605918c7f5 --- /dev/null +++ b/src/sentry/runner/commands/createorg.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import click + +from sentry.runner.decorators import configuration + + +@click.command() +@click.option("--name", required=True, help="Organization name.") +@click.option( + "--slug", + default=None, + help="URL-friendly slug. Derived from name if omitted.", +) +@click.option("--owner-email", required=True, help="Email of the organization owner.") +@click.option( + "--no-default-team", + default=False, + is_flag=True, + help="Skip creating a default team.", +) +@configuration +def createorg( + name: str, + slug: str | None, + owner_email: str, + no_default_team: bool, +) -> None: + "Create a new organization." + + from sentry.services.organization.model import ( + OrganizationOptions, + OrganizationProvisioningOptions, + PostProvisionOptions, + ) + from sentry.services.organization.provisioning import ( + OrganizationProvisioningException, + organization_provisioning_service, + ) + + provision_args = OrganizationProvisioningOptions( + provision_options=OrganizationOptions( + name=name, + slug=slug or name, + owning_email=owner_email, + create_default_team=not no_default_team, + ), + post_provision_options=PostProvisionOptions(), + ) + + try: + rpc_org = organization_provisioning_service.provision_organization_in_cell( + provisioning_options=provision_args, + ) + except OrganizationProvisioningException as e: + raise click.ClickException(str(e)) + + click.echo(f"Organization created: {rpc_org.name} (slug: {rpc_org.slug}, id: {rpc_org.id})") diff --git a/src/sentry/runner/main.py b/src/sentry/runner/main.py index 821ba6350a47..5d3daab4760f 100644 --- a/src/sentry/runner/main.py +++ b/src/sentry/runner/main.py @@ -45,6 +45,7 @@ def cli(config: str) -> None: "sentry.runner.commands.configoptions.configoptions", "sentry.runner.commands.createflag.createflag", "sentry.runner.commands.createflag.createissueflag", + "sentry.runner.commands.createorg.createorg", "sentry.runner.commands.createuser.createuser", "sentry.runner.commands.devserver.devserver", "sentry.runner.commands.django.django", diff --git a/tests/sentry/runner/commands/test_createorg.py b/tests/sentry/runner/commands/test_createorg.py new file mode 100644 index 000000000000..a603943721f1 --- /dev/null +++ b/tests/sentry/runner/commands/test_createorg.py @@ -0,0 +1,47 @@ +from sentry.models.organization import Organization +from sentry.models.organizationmember import OrganizationMember +from sentry.models.team import Team +from sentry.runner.commands.createorg import createorg +from sentry.testutils.cases import CliTestCase + + +class CreateOrgTest(CliTestCase): + command = createorg + + def test_basic_create(self) -> None: + rv = self.invoke("--name=Test Org", "--slug=test-org", "--owner-email=owner@example.com") + assert rv.exit_code == 0, rv.output + assert "test-org" in rv.output + + org = Organization.objects.get(slug="test-org") + assert org.name == "Test Org" + assert OrganizationMember.objects.filter( + organization=org, email="owner@example.com" + ).exists() + assert Team.objects.filter(organization=org).exists() + + def test_slug_defaults_to_name(self) -> None: + rv = self.invoke("--name=My Organization", "--owner-email=owner@example.com") + assert rv.exit_code == 0, rv.output + + assert Organization.objects.filter(slug="my-organization").exists() + + def test_no_default_team(self) -> None: + rv = self.invoke( + "--name=No Team Org", + "--slug=no-team-org", + "--owner-email=owner@example.com", + "--no-default-team", + ) + assert rv.exit_code == 0, rv.output + + org = Organization.objects.get(slug="no-team-org") + assert not Team.objects.filter(organization=org).exists() + + def test_missing_name(self) -> None: + rv = self.invoke("--slug=test-org", "--owner-email=owner@example.com") + assert rv.exit_code != 0 + + def test_missing_owner_email(self) -> None: + rv = self.invoke("--name=Test Org", "--slug=test-org") + assert rv.exit_code != 0 From 486e7c602c0cd3d398b4a008c03b8d45c89d72b5 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Mon, 30 Mar 2026 12:53:46 -0400 Subject: [PATCH 12/57] fix(tracemetrics): Allow delete for big number when more than 1 field (#111791) There's a state for metric widgets and release health widgets where if you have multiple y-axes selected, then switch to a big number widget, we don't trim off the y-axes that are selected, instead we just render them with a radio button. This is meant for datasets that support equations, but at the moment a user can get "stuck" in the big number widget flow since this delete button is hidden. I figured we should just show it if there are more than 1 fields, even if equations aren't supported. One day we should fix how big numbers are stored if there are multiply y-axes, but this just patches the behaviour so we can release multi-metric selection because I imagine it'll raise feedback. --- .../dashboards/widgetBuilder/components/visualize/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx b/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx index 843f78d7f4fd..af05cd4efc2b 100644 --- a/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx @@ -1023,7 +1023,9 @@ export function Visualize({error, setError}: VisualizeProps) { /> )} - {(!isBigNumberWidget || datasetConfig.enableEquations) && ( + {(!isBigNumberWidget || + datasetConfig.enableEquations || + (isBigNumberWidget && fields.length > 1)) && (