From 85be6fc406f73ae96a2d2d28e6b528e555f8b956 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 10 Nov 2025 12:56:52 -0500 Subject: [PATCH 1/4] feat(metrics): Add metrics section to issue details similar to ourlogs --- .../events/metrics/metricsDrawer.tsx | 71 ++++ .../events/metrics/metricsSection.spec.tsx | 330 ++++++++++++++++++ .../events/metrics/metricsSection.tsx | 140 ++++++++ .../utils/analytics/metricsAnalyticsEvent.tsx | 4 + .../app/views/explore/logs/useLogsQuery.tsx | 9 +- .../metricInfoTabs/metricInfoTabStyles.tsx | 4 +- .../metricInfoTabs/metricsSamplesTable.tsx | 9 +- .../metricInfoTabs/metricsSamplesTableRow.tsx | 2 +- .../groupEventDetailsContent.tsx | 6 + .../views/issueDetails/streamline/context.tsx | 1 + .../streamline/issueDetailsJumpTo.tsx | 4 + 11 files changed, 570 insertions(+), 10 deletions(-) create mode 100644 static/app/components/events/metrics/metricsDrawer.tsx create mode 100644 static/app/components/events/metrics/metricsSection.spec.tsx create mode 100644 static/app/components/events/metrics/metricsSection.tsx diff --git a/static/app/components/events/metrics/metricsDrawer.tsx b/static/app/components/events/metrics/metricsDrawer.tsx new file mode 100644 index 00000000000000..c53a193f3e7776 --- /dev/null +++ b/static/app/components/events/metrics/metricsDrawer.tsx @@ -0,0 +1,71 @@ +import {useRef} from 'react'; + +import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar'; +import { + CrumbContainer, + EventDrawerBody, + EventDrawerContainer, + EventDrawerHeader, + EventNavigator, + NavigationCrumbs, + ShortId, +} from 'sentry/components/events/eventDrawer'; +import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; +import {t} from 'sentry/locale'; +import type {Event} from 'sentry/types/event'; +import type {Group} from 'sentry/types/group'; +import type {Project} from 'sentry/types/project'; +import {getShortEventId} from 'sentry/utils/events'; +import {MetricsSamplesTable} from 'sentry/views/explore/metrics/metricInfoTabs/metricsSamplesTable'; +import { + useQueryParamsSearch, + useSetQueryParamsQuery, +} from 'sentry/views/explore/queryParams/context'; + +interface MetricsIssueDrawerProps { + event: Event; + group: Group; + project: Project; +} + +export function MetricsDrawer({event, project, group}: MetricsIssueDrawerProps) { + const setMetricsQuery = useSetQueryParamsQuery(); + const metricsSearch = useQueryParamsSearch(); + const containerRef = useRef(null); + + return ( + + + + + {group.shortId} + + ), + }, + {label: getShortEventId(event.id)}, + {label: t('Metrics')}, + ]} + /> + + + new Promise(() => [])} + initialQuery={metricsSearch.formatString()} + searchSource="tracemetrics" + onSearch={query => setMetricsQuery(query)} + /> + + +
+ +
+
+
+ ); +} diff --git a/static/app/components/events/metrics/metricsSection.spec.tsx b/static/app/components/events/metrics/metricsSection.spec.tsx new file mode 100644 index 00000000000000..5d4c76de86b973 --- /dev/null +++ b/static/app/components/events/metrics/metricsSection.spec.tsx @@ -0,0 +1,330 @@ +import {EventFixture} from 'sentry-fixture/event'; +import {GroupFixture} from 'sentry-fixture/group'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import { + render, + screen, + userEvent, + waitFor, + within, +} from 'sentry-test/reactTestingLibrary'; + +import {MetricsSection} from 'sentry/components/events/metrics/metricsSection'; +import PageFiltersStore from 'sentry/stores/pageFiltersStore'; +import ProjectsStore from 'sentry/stores/projectsStore'; +import {TraceMetricKnownFieldKey} from 'sentry/views/explore/metrics/types'; + +const TRACE_ID = '00000000000000000000000000000000'; + +const organization = OrganizationFixture({ + features: ['tracemetrics-enabled'], +}); + +const project = ProjectFixture(); +const group = GroupFixture(); + +const event = EventFixture({ + id: '11111111111111111111111111111111', + dateCreated: '2025-01-01T12:00:00.000Z', + contexts: { + trace: { + trace_id: TRACE_ID, + span_id: '1111111111111111', + op: 'ui.action.click', + type: 'trace', + }, + }, +}); + +describe('MetricsSection', () => { + let metricId: string; + let mockRequest: jest.Mock; + + beforeEach(() => { + metricId = '22222222222222222222222222222222'; + + ProjectsStore.loadInitialData([project]); + + PageFiltersStore.init(); + PageFiltersStore.onInitializeUrlState({ + projects: [parseInt(project.id, 10)], + environments: [], + datetime: { + period: '14d', + start: null, + end: null, + utc: null, + }, + }); + + MockApiClient.addMockResponse({ + url: `/projects/`, + body: [project], + }); + + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/`, + body: project, + }); + + mockRequest = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: { + data: [ + { + [TraceMetricKnownFieldKey.ID]: metricId, + [TraceMetricKnownFieldKey.PROJECT_ID]: project.id, + [TraceMetricKnownFieldKey.ORGANIZATION_ID]: Number(organization.id), + [TraceMetricKnownFieldKey.TRACE]: TRACE_ID, + [TraceMetricKnownFieldKey.METRIC_NAME]: 'http.server.duration', + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'distribution', + [TraceMetricKnownFieldKey.METRIC_VALUE]: 150.5, + [TraceMetricKnownFieldKey.TIMESTAMP]: '2025-01-01T12:00:00.000Z', + }, + ], + meta: { + fields: { + [TraceMetricKnownFieldKey.METRIC_NAME]: 'string', + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'string', + [TraceMetricKnownFieldKey.METRIC_VALUE]: 'number', + }, + units: {}, + }, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/trace-items/attributes/`, + method: 'GET', + body: {}, + }); + + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); + + // Mock telemetry requests for errors, spans, and logs + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + match: [MockApiClient.matchQuery({dataset: 'errors'})], + body: {data: [], meta: {}}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + match: [MockApiClient.matchQuery({dataset: 'spansIndexed'})], + body: {data: [], meta: {}}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + match: [MockApiClient.matchQuery({dataset: 'ourlogs'})], + body: {data: [], meta: {}}, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders empty when no trace id', () => { + const eventWithoutTrace = EventFixture({ + contexts: {}, + }); + + render(, { + organization, + }); + + expect(screen.queryByText(/Metrics/)).not.toBeInTheDocument(); + }); + + it('does not render when feature flag is disabled', () => { + const orgWithoutFeature = OrganizationFixture({ + features: [], + }); + + render(, { + organization: orgWithoutFeature, + }); + + expect(screen.queryByText(/Metrics/)).not.toBeInTheDocument(); + }); + + it('renders empty when no metrics data', () => { + const mockRequestEmpty = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: { + data: [], + meta: {}, + }, + }); + + render(, { + organization, + }); + + expect(mockRequestEmpty).toHaveBeenCalledTimes(1); + expect(screen.queryByText(/Metrics/)).not.toBeInTheDocument(); + }); + + it('renders metrics section with data', async () => { + render(, { + organization, + initialRouterConfig: { + location: { + pathname: `/organizations/${organization.slug}/issues/${group.id}/`, + query: { + project: project.id, + }, + }, + }, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(screen.getByText(/Metrics/)).toBeInTheDocument(); + }); + + expect(screen.getByTestId('metrics')).toBeInTheDocument(); + }); + + it('shows view more button when there are more than 5 metrics', async () => { + const mockRequestWithManyMetrics = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: { + data: Array.from({length: 10}, (_, i) => ({ + [TraceMetricKnownFieldKey.ID]: `metric-${i}`, + [TraceMetricKnownFieldKey.PROJECT_ID]: project.id, + [TraceMetricKnownFieldKey.ORGANIZATION_ID]: Number(organization.id), + [TraceMetricKnownFieldKey.TRACE]: TRACE_ID, + [TraceMetricKnownFieldKey.METRIC_NAME]: `metric.name.${i}`, + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'counter', + [TraceMetricKnownFieldKey.METRIC_VALUE]: i * 10, + [TraceMetricKnownFieldKey.TIMESTAMP]: '2025-01-01T12:00:00.000Z', + })), + meta: { + fields: { + [TraceMetricKnownFieldKey.METRIC_NAME]: 'string', + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'string', + [TraceMetricKnownFieldKey.METRIC_VALUE]: 'number', + }, + units: {}, + }, + }, + }); + + render(, { + organization, + }); + + expect(mockRequestWithManyMetrics).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(screen.getByText(/Metrics/)).toBeInTheDocument(); + }); + + expect(screen.getByRole('button', {name: 'View more'})).toBeInTheDocument(); + }); + + it('opens metrics drawer when view more is clicked', async () => { + const mockRequestWithManyMetrics = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: { + data: Array.from({length: 10}, (_, i) => ({ + [TraceMetricKnownFieldKey.ID]: `metric-${i}`, + [TraceMetricKnownFieldKey.PROJECT_ID]: project.id, + [TraceMetricKnownFieldKey.ORGANIZATION_ID]: Number(organization.id), + [TraceMetricKnownFieldKey.TRACE]: TRACE_ID, + [TraceMetricKnownFieldKey.METRIC_NAME]: `metric.name.${i}`, + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'counter', + [TraceMetricKnownFieldKey.METRIC_VALUE]: i * 10, + [TraceMetricKnownFieldKey.TIMESTAMP]: '2025-01-01T12:00:00.000Z', + })), + meta: { + fields: { + [TraceMetricKnownFieldKey.METRIC_NAME]: 'string', + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'string', + [TraceMetricKnownFieldKey.METRIC_VALUE]: 'number', + }, + units: {}, + }, + }, + }); + + render(, { + organization, + initialRouterConfig: { + location: { + pathname: `/organizations/${organization.slug}/issues/${group.id}/`, + query: { + project: project.id, + }, + }, + }, + }); + + expect(mockRequestWithManyMetrics).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(screen.getByText(/Metrics/)).toBeInTheDocument(); + }); + + expect( + screen.queryByRole('complementary', {name: 'metrics drawer'}) + ).not.toBeInTheDocument(); + + await userEvent.click(screen.getByRole('button', {name: 'View more'})); + + const aside = screen.getByRole('complementary', {name: 'metrics drawer'}); + expect(aside).toBeInTheDocument(); + + // Check that the drawer contains the expected elements + expect(within(aside).getByText('Metrics')).toBeInTheDocument(); + expect( + within(aside).getByPlaceholderText('Search metrics for this trace') + ).toBeInTheDocument(); + }); + + it('does not show view more button when there are 5 or fewer metrics', async () => { + const mockRequestWithFewMetrics = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: { + data: Array.from({length: 3}, (_, i) => ({ + [TraceMetricKnownFieldKey.ID]: `metric-${i}`, + [TraceMetricKnownFieldKey.PROJECT_ID]: project.id, + [TraceMetricKnownFieldKey.ORGANIZATION_ID]: Number(organization.id), + [TraceMetricKnownFieldKey.TRACE]: TRACE_ID, + [TraceMetricKnownFieldKey.METRIC_NAME]: `metric.name.${i}`, + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'counter', + [TraceMetricKnownFieldKey.METRIC_VALUE]: i * 10, + [TraceMetricKnownFieldKey.TIMESTAMP]: '2025-01-01T12:00:00.000Z', + })), + meta: { + fields: { + [TraceMetricKnownFieldKey.METRIC_NAME]: 'string', + [TraceMetricKnownFieldKey.METRIC_TYPE]: 'string', + [TraceMetricKnownFieldKey.METRIC_VALUE]: 'number', + }, + units: {}, + }, + }, + }); + + render(, { + organization, + }); + + expect(mockRequestWithFewMetrics).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(screen.getByText(/Metrics/)).toBeInTheDocument(); + }); + + expect(screen.queryByRole('button', {name: 'View more'})).not.toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/metrics/metricsSection.tsx b/static/app/components/events/metrics/metricsSection.tsx new file mode 100644 index 00000000000000..6eeb37771163fd --- /dev/null +++ b/static/app/components/events/metrics/metricsSection.tsx @@ -0,0 +1,140 @@ +import {useCallback, useRef} from 'react'; + +import {Flex} from '@sentry/scraps/layout/flex'; + +import {Button} from 'sentry/components/core/button'; +import {MetricsDrawer} from 'sentry/components/events/metrics/metricsDrawer'; +import useDrawer from 'sentry/components/globalDrawer'; +import {IconChevron} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import type {Event} from 'sentry/types/event'; +import type {Group} from 'sentry/types/group'; +import type {Project} from 'sentry/types/project'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import useOrganization from 'sentry/utils/useOrganization'; +import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; +import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetricSamplesTable'; +import {MetricsSamplesTable} from 'sentry/views/explore/metrics/metricInfoTabs/metricsSamplesTable'; +import {canUseMetricsUI} from 'sentry/views/explore/metrics/metricsFlags'; +import {TraceItemDataset} from 'sentry/views/explore/types'; +import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; +import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; +import {TraceViewMetricsDataProvider} from 'sentry/views/performance/newTraceDetails/traceMetrics'; + +export function MetricsSection({ + event, + project, + group, +}: { + event: Event; + group: Group; + project: Project; +}) { + const traceId = event.contexts?.trace?.trace_id; + + if (!traceId) { + return null; + } + + return ( + + + + ); +} + +function MetricsSectionContent({ + event, + project, + group, +}: { + event: Event; + group: Group; + project: Project; +}) { + const organization = useOrganization(); + const feature = canUseMetricsUI(organization); + const {openDrawer} = useDrawer(); + const viewAllButtonRef = useRef(null); + const traceId = event.contexts?.trace?.trace_id; + + const { + result: {data: tableData}, + } = useMetricSamplesTable({ + disabled: !traceId, + limit: 5, + traceMetric: undefined, + fields: [], + ingestionDelaySeconds: 120, + }); + + const onOpenMetricsDrawer = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + trackAnalytics('metrics.issue_details.drawer_opened', { + organization, + }); + openDrawer( + () => ( + + + + + + ), + { + ariaLabel: 'metrics drawer', + drawerKey: 'metrics-issue-drawer', + shouldCloseOnInteractOutside: element => { + const viewAllButton = viewAllButtonRef.current; + return !viewAllButton?.contains(element); + }, + } + ); + }, + [group, event, project, openDrawer, organization, traceId] + ); + + if (!feature) { + return null; + } + + if (!traceId) { + // If there isn't a traceId, we shouldn't show metrics since they are trace specific + return null; + } + + if (!tableData || tableData.length === 0) { + // Don't show the metrics section if there are no metrics + return null; + } + + return ( + + + + {tableData && tableData.length > 5 ? ( +
+ +
+ ) : null} +
+
+ ); +} diff --git a/static/app/utils/analytics/metricsAnalyticsEvent.tsx b/static/app/utils/analytics/metricsAnalyticsEvent.tsx index f96855b6f9651d..2fc1d3245a67f0 100644 --- a/static/app/utils/analytics/metricsAnalyticsEvent.tsx +++ b/static/app/utils/analytics/metricsAnalyticsEvent.tsx @@ -34,6 +34,9 @@ export type MetricsAnalyticsEventParameters = { platform: PlatformKey | 'unknown'; supports_onboarding_checklist: boolean; }; + 'metrics.issue_details.drawer_opened': { + organization: Organization; + }; 'metrics.nav.rendered': { metrics_tab_visible: boolean; organization: Organization; @@ -66,6 +69,7 @@ type MetricsAnalyticsEventKey = keyof MetricsAnalyticsEventParameters; export const metricsAnalyticsEventMap: Record = { 'metrics.explorer.metadata': 'Metric Explorer Pageload Metadata', 'metrics.explorer.panel.metadata': 'Metric Explorer Panel Metadata', + 'metrics.issue_details.drawer_opened': 'Metrics Issue Details Drawer Opened', 'metrics.explorer.setup_button_clicked': 'Metrics Setup Button Clicked', 'metrics.nav.rendered': 'Metrics Nav Rendered', 'metrics.onboarding': 'Metrics Explore Empty State (Onboarding)', diff --git a/static/app/views/explore/logs/useLogsQuery.tsx b/static/app/views/explore/logs/useLogsQuery.tsx index 8b75b07e6733ba..856bdd345151de 100644 --- a/static/app/views/explore/logs/useLogsQuery.tsx +++ b/static/app/views/explore/logs/useLogsQuery.tsx @@ -218,7 +218,10 @@ function getPageParam( } : undefined; - if (highFidelity || isFlexTimePageParam(pageParam)) { + if ( + (highFidelity && !autoRefresh) || + (isFlexTimePageParam(pageParam) && !autoRefresh) + ) { const pageLinkHeader = response?.getResponseHeader('Link') ?? null; const links = parseLinkHeader(pageLinkHeader); const link = isGetPreviousPage ? links.previous : links.next; @@ -266,8 +269,8 @@ function getPageParam( const timestampPrecise = isGetPreviousPage ? firstTimestamp : lastTimestamp; const indexFromInitialPage = isGetPreviousPage - ? (pageParam?.indexFromInitialPage ?? 0) - 1 - : (pageParam?.indexFromInitialPage ?? 0) + 1; + ? ((pageParam as InfiniteScrollPageParam)?.indexFromInitialPage ?? 0) - 1 + : ((pageParam as InfiniteScrollPageParam)?.indexFromInitialPage ?? 0) + 1; const pageParamResult: InfiniteScrollPageParam = { logId, diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx index eed4f4493a07fb..83d518246c76e8 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx @@ -100,10 +100,10 @@ export const StyledSimpleTableHeader = styled(SimpleTable.Header)` `; export const StickyTableRow = styled(SimpleTable.Row)<{ - isSticky?: boolean; + sticky?: boolean; }>` ${p => - p.isSticky && + p.sticky && ` top: 0px; z-index: 1; diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx index 27b5cbc2c8e719..ad61436fc7772b 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx @@ -74,7 +74,7 @@ export function MetricsSamplesTable({ {error ? ( - + ) : data?.length ? ( @@ -89,11 +89,11 @@ export function MetricsSamplesTable({ /> )) ) : isFetching ? ( - - + + ) : ( - + )} @@ -107,6 +107,7 @@ const SimpleTableWithHiddenColumns = styled(StyledSimpleTable)<{ numColumns: number; }>` grid-template-columns: repeat(${p => p.numColumns}, min-content) 1fr; + grid-column: 1 / -1; ${p => !p.embedded && diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx index 5acef3d7151ed9..905b6f68a7d798 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx @@ -302,7 +302,7 @@ export function SampleTableRow({ return ( - + {columns.map((field, i) => { const isValueColumn = field === TraceMetricKnownFieldKey.METRIC_VALUE; const cellContent = renderFieldCell(field); diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index b850f483358280..e11abc5bf01a00 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -50,6 +50,7 @@ import {StackTrace} from 'sentry/components/events/interfaces/stackTrace'; import {Template} from 'sentry/components/events/interfaces/template'; import {Threads} from 'sentry/components/events/interfaces/threads'; import {UptimeDataSection} from 'sentry/components/events/interfaces/uptime/uptimeDataSection'; +import {MetricsSection} from 'sentry/components/events/metrics/metricsSection'; import {OurlogsSection} from 'sentry/components/events/ourlogs/ourlogsSection'; import {EventPackageData} from 'sentry/components/events/packageData'; import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration'; @@ -373,6 +374,11 @@ export function EventDetailsContent({ + + + + + {hasStreamlinedUI && event.contexts.trace?.trace_id && organization.features.includes('performance-view') && ( diff --git a/static/app/views/issueDetails/streamline/context.tsx b/static/app/views/issueDetails/streamline/context.tsx index 3e2e88b40125dd..be5b7d7bee3689 100644 --- a/static/app/views/issueDetails/streamline/context.tsx +++ b/static/app/views/issueDetails/streamline/context.tsx @@ -53,6 +53,7 @@ export const enum SectionKey { BREADCRUMBS = 'breadcrumbs', LOGS = 'logs', + METRICS = 'metrics', SPAN_ATTRIBUTES = 'span-attributes', /** * Also called images loaded diff --git a/static/app/views/issueDetails/streamline/issueDetailsJumpTo.tsx b/static/app/views/issueDetails/streamline/issueDetailsJumpTo.tsx index 9ab5aed9e66ec6..f0c386968cf8e5 100644 --- a/static/app/views/issueDetails/streamline/issueDetailsJumpTo.tsx +++ b/static/app/views/issueDetails/streamline/issueDetailsJumpTo.tsx @@ -23,6 +23,7 @@ const sectionLabels: Partial> = { [SectionKey.BREADCRUMBS]: t('Breadcrumbs'), [SectionKey.TRACE]: t('Trace'), [SectionKey.LOGS]: t('Logs'), + [SectionKey.METRICS]: t('Metrics'), [SectionKey.TAGS]: t('Tags'), [SectionKey.CONTEXTS]: t('Context'), [SectionKey.USER_FEEDBACK]: t('User Feedback'), @@ -42,6 +43,9 @@ export function IssueDetailsJumpTo() { if (!features.includes('ourlogs-enabled')) { excluded.push(SectionKey.LOGS); } + if (!features.includes('tracemetrics-enabled')) { + excluded.push(SectionKey.METRICS); + } return excluded; }, [organization.features]); From 4f0cb761ce8e324f047700b2a19e85422f8ea9d5 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 10 Nov 2025 15:21:56 -0500 Subject: [PATCH 2/4] Make and use generic provider for state based single query --- .../events/metrics/metricsSection.tsx | 40 ++++---- .../events/metrics/useMetricsIssueSection.tsx | 15 +++ .../exploreStateQueryParamsProvider.tsx | 95 +++++++++++++++++++ .../logs/logsStateQueryParamsProvider.tsx | 77 ++------------- .../app/views/explore/logs/useLogsQuery.tsx | 9 +- .../metricInfoTabs/metricsSamplesTable.tsx | 16 +++- .../app/views/explore/metrics/metricQuery.tsx | 18 ++-- .../explore/metrics/metricsQueryParams.tsx | 11 ++- .../metricsStateQueryParamsProvider.tsx | 32 +++++++ .../metrics/multiMetricsQueryParams.tsx | 21 ---- .../performance/newTraceDetails/index.tsx | 6 +- .../newTraceDetails/traceMetrics.tsx | 52 ++++------ 12 files changed, 228 insertions(+), 164 deletions(-) create mode 100644 static/app/components/events/metrics/useMetricsIssueSection.tsx create mode 100644 static/app/views/explore/exploreStateQueryParamsProvider.tsx create mode 100644 static/app/views/explore/metrics/metricsStateQueryParamsProvider.tsx diff --git a/static/app/components/events/metrics/metricsSection.tsx b/static/app/components/events/metrics/metricsSection.tsx index 6eeb37771163fd..8e99a3f9c433f5 100644 --- a/static/app/components/events/metrics/metricsSection.tsx +++ b/static/app/components/events/metrics/metricsSection.tsx @@ -4,6 +4,7 @@ import {Flex} from '@sentry/scraps/layout/flex'; import {Button} from 'sentry/components/core/button'; import {MetricsDrawer} from 'sentry/components/events/metrics/metricsDrawer'; +import {useMetricsIssueSection} from 'sentry/components/events/metrics/useMetricsIssueSection'; import useDrawer from 'sentry/components/globalDrawer'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -13,13 +14,12 @@ import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import useOrganization from 'sentry/utils/useOrganization'; import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; -import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetricSamplesTable'; import {MetricsSamplesTable} from 'sentry/views/explore/metrics/metricInfoTabs/metricsSamplesTable'; import {canUseMetricsUI} from 'sentry/views/explore/metrics/metricsFlags'; import {TraceItemDataset} from 'sentry/views/explore/types'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; -import {TraceViewMetricsDataProvider} from 'sentry/views/performance/newTraceDetails/traceMetrics'; +import {TraceViewMetricsProviderWrapper} from 'sentry/views/performance/newTraceDetails/traceMetrics'; export function MetricsSection({ event, @@ -37,9 +37,14 @@ export function MetricsSection({ } return ( - - - + + + ); } @@ -47,26 +52,19 @@ function MetricsSectionContent({ event, project, group, + traceId, }: { event: Event; group: Group; project: Project; + traceId: string; }) { const organization = useOrganization(); const feature = canUseMetricsUI(organization); const {openDrawer} = useDrawer(); const viewAllButtonRef = useRef(null); - const traceId = event.contexts?.trace?.trace_id; - - const { - result: {data: tableData}, - } = useMetricSamplesTable({ - disabled: !traceId, - limit: 5, - traceMetric: undefined, - fields: [], - ingestionDelaySeconds: 120, - }); + const {result} = useMetricsIssueSection({traceId}); + const abbreviatedTableData = result.data ? result.data.slice(0, 5) : undefined; const onOpenMetricsDrawer = useCallback( (e: React.MouseEvent) => { @@ -76,14 +74,14 @@ function MetricsSectionContent({ }); openDrawer( () => ( - + - + ), { ariaLabel: 'metrics drawer', @@ -107,7 +105,7 @@ function MetricsSectionContent({ return null; } - if (!tableData || tableData.length === 0) { + if (!result.data || result.data.length === 0) { // Don't show the metrics section if there are no metrics return null; } @@ -120,8 +118,8 @@ function MetricsSectionContent({ data-test-id="metrics-data-section" > - - {tableData && tableData.length > 5 ? ( + + {result.data && result.data.length > 5 ? (