Skip to content

Commit 9a547bd

Browse files
authored
feat(tracemetrics): Add trace view for metrics (#102964)
Allows linking into the metrics tab in trace view and shows metrics in a list.
1 parent 8d44dd2 commit 9a547bd

20 files changed

+481
-62
lines changed

static/app/router/routes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,7 @@ function buildRoutes(): RouteObject[] {
22192219
index: true,
22202220
component: make(() => import('sentry/views/explore/metrics/content')),
22212221
},
2222+
traceView,
22222223
];
22232224

22242225
const profilingChildren: SentryRouteObject[] = [

static/app/views/explore/metrics/constants.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,17 @@ export const TraceSamplesTableEmbeddedColumns: Array<
7272
> = [
7373
VirtualTableSampleColumnKey.EXPAND_ROW,
7474
TraceMetricKnownFieldKey.TIMESTAMP,
75+
VirtualTableSampleColumnKey.PROJECT_BADGE,
7576
TraceMetricKnownFieldKey.METRIC_NAME,
77+
TraceMetricKnownFieldKey.METRIC_TYPE,
7678
TraceMetricKnownFieldKey.METRIC_VALUE,
7779
];
7880

81+
export const NoPaddingColumns: VirtualTableSampleColumnKey[] = [
82+
VirtualTableSampleColumnKey.EXPAND_ROW,
83+
VirtualTableSampleColumnKey.PROJECT_BADGE,
84+
];
85+
7986
export const OPTIONS_BY_TYPE: Record<string, Array<SelectOption<string>>> = {
8087
counter: [
8188
{

static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,33 +53,31 @@ export const WrappingText = styled('div')`
5353

5454
export const ExpandedRowContainer = styled('div')<{embedded?: boolean}>`
5555
grid-column: 1 / -1;
56-
border-bottom: 1px solid ${p => p.theme.innerBorder};
57-
58-
${p =>
59-
p.embedded &&
60-
css`
61-
padding: ${p.theme.space.xs} ${p.theme.space.sm};
62-
`}
6356
`;
6457

6558
export const StyledSimpleTableRowCell = styled(SimpleTable.RowCell)<{
6659
embedded?: boolean;
6760
noPadding?: boolean;
6861
}>`
69-
padding: ${p => (p.noPadding ? 0 : p.theme.space.lg)};
70-
padding-top: ${p => (p.noPadding ? 0 : p.theme.space.xs)};
71-
padding-bottom: ${p => (p.noPadding ? 0 : p.theme.space.xs)};
62+
padding: ${p => (p.noPadding ? 0 : p.embedded ? p.theme.space.xl : p.theme.space.lg)};
63+
padding-top: ${p =>
64+
p.noPadding ? 0 : p.embedded ? p.theme.space.sm : p.theme.space.xs};
65+
padding-bottom: ${p =>
66+
p.noPadding ? 0 : p.embedded ? p.theme.space.sm : p.theme.space.xs};
7267
7368
font-size: ${p => p.theme.fontSize.sm};
7469
`;
7570

7671
export const StyledSimpleTableHeaderCell = styled(SimpleTable.HeaderCell)<{
72+
embedded?: boolean;
7773
noPadding?: boolean;
7874
}>`
7975
font-size: ${p => p.theme.fontSize.sm};
80-
padding: ${p => (p.noPadding ? 0 : p.theme.space.lg)};
81-
padding-top: ${p => (p.noPadding ? 0 : p.theme.space.xs)};
82-
padding-bottom: ${p => (p.noPadding ? 0 : p.theme.space.xs)};
76+
padding: ${p => (p.noPadding ? 0 : p.embedded ? p.theme.space.xl : p.theme.space.lg)};
77+
padding-top: ${p =>
78+
p.noPadding ? 0 : p.embedded ? p.theme.space.sm : p.theme.space.xs};
79+
padding-bottom: ${p =>
80+
p.noPadding ? 0 : p.embedded ? p.theme.space.sm : p.theme.space.xs};
8381
`;
8482

8583
export const StyledSimpleTableBody = styled('div')`
@@ -112,6 +110,8 @@ export const StickyTableRow = styled(SimpleTable.Row)<{
112110
background: ${p.theme.background};
113111
position: sticky;
114112
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
113+
margin-right: -15px;
114+
padding-right: calc(15px);
115115
`}
116116
`;
117117

@@ -124,6 +124,9 @@ export const DetailsContent = styled(StyledPanel)`
124124

125125
export const MetricsDetailsWrapper = styled(DetailsWrapper)`
126126
border-top: 0;
127+
border-bottom: 0;
128+
margin-right: -15px;
129+
padding-right: calc(15px + ${p => p.theme.space.md});
127130
`;
128131

129132
export const NumericSimpleTableHeaderCell = styled(StyledSimpleTableHeaderCell)`
@@ -144,3 +147,17 @@ export const BodyContainer = styled('div')`
144147
export const StyledTabPanels = styled(TabPanels)`
145148
overflow: auto;
146149
`;
150+
151+
export const TableRowContainer = styled('div')`
152+
display: grid;
153+
grid-template-columns: subgrid;
154+
grid-auto-rows: min-content;
155+
grid-column: 1 / -1;
156+
157+
:not(:last-child) {
158+
border-bottom: 1px solid ${p => p.theme.border};
159+
}
160+
161+
margin-right: -15px;
162+
padding-right: calc(15px);
163+
`;

static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function MetricsSamplesTable({
6969
return (
7070
<SimpleTableWithHiddenColumns numColumns={columns.length - 1} embedded={embedded}>
7171
{isFetching && <TransparentLoadingMask />}
72-
<MetricsSamplesTableHeader columns={columns} />
72+
<MetricsSamplesTableHeader columns={columns} embedded={embedded} />
7373
<StyledSimpleTableBody>
7474
{error ? (
7575
<SimpleTable.Empty>

static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableHeader.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {ReactNode} from 'react';
33
import {Tooltip} from 'sentry/components/core/tooltip';
44
import {IconFire, IconSpan, IconTerminal} from 'sentry/icons';
55
import {t} from 'sentry/locale';
6+
import {NoPaddingColumns} from 'sentry/views/explore/metrics/constants';
67
import {
78
NumericSimpleTableHeaderCell,
89
StyledSimpleTableHeader,
@@ -24,9 +25,13 @@ const ICON_HEADERS = {
2425

2526
interface MetricsSamplesTableHeaderProps {
2627
columns: SampleTableColumnKey[];
28+
embedded?: boolean;
2729
}
2830

29-
export function MetricsSamplesTableHeader({columns}: MetricsSamplesTableHeaderProps) {
31+
export function MetricsSamplesTableHeader({
32+
columns,
33+
embedded,
34+
}: MetricsSamplesTableHeaderProps) {
3035
const sorts = useQueryParamsSortBys();
3136

3237
return (
@@ -41,6 +46,7 @@ export function MetricsSamplesTableHeader({columns}: MetricsSamplesTableHeaderPr
4146
field={field}
4247
index={i}
4348
sort={sorts.find(s => s.field === field)?.kind}
49+
embedded={embedded}
4450
>
4551
{columnType === 'stat'
4652
? ICON_HEADERS[field as keyof typeof ICON_HEADERS]
@@ -59,22 +65,25 @@ function FieldHeaderCellWrapper({
5965
children,
6066
index,
6167
sort,
68+
embedded = false,
6269
}: {
6370
children: ReactNode;
6471
field: SampleTableColumnKey;
6572
index: number;
73+
embedded?: boolean;
6674
sort?: 'asc' | 'desc';
6775
}) {
6876
const columnType = getMetricTableColumnType(field);
6977
const label = getFieldLabel(field);
70-
const hasPadding = field !== VirtualTableSampleColumnKey.EXPAND_ROW;
78+
const hasPadding = !NoPaddingColumns.includes(field as VirtualTableSampleColumnKey);
7179

7280
if (columnType === 'stat') {
7381
return (
7482
<NumericSimpleTableHeaderCell
7583
key={`stat-${index}`}
7684
divider={false}
7785
data-column-name={field}
86+
embedded={embedded}
7887
>
7988
<Tooltip title={label} skipWrapper>
8089
{children}
@@ -92,6 +101,7 @@ function FieldHeaderCellWrapper({
92101
justifyContent: 'flex-end',
93102
paddingRight: 'calc(12px + 15px)', // 12px is the padding of the cell, 15px is the width of the scrollbar.
94103
}}
104+
embedded={embedded}
95105
>
96106
<Tooltip showOnlyOnOverflow title={label}>
97107
{children}
@@ -101,7 +111,12 @@ function FieldHeaderCellWrapper({
101111
}
102112

103113
return (
104-
<StyledSimpleTableHeaderCell key={index} sort={sort} noPadding={!hasPadding}>
114+
<StyledSimpleTableHeaderCell
115+
key={index}
116+
sort={sort}
117+
noPadding={!hasPadding}
118+
embedded={embedded}
119+
>
105120
<Tooltip showOnlyOnOverflow title={label}>
106121
{children}
107122
</Tooltip>

static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {useRef, useState, type ReactNode} from 'react';
22
import {useTheme} from '@emotion/react';
3-
import styled from '@emotion/styled';
3+
4+
import {Flex} from '@sentry/scraps/layout/flex';
45

56
import {Button} from 'sentry/components/core/button';
67
import {Link} from 'sentry/components/core/link';
78
import {Tooltip} from 'sentry/components/core/tooltip';
89
import Count from 'sentry/components/count';
10+
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
911
import {IconChevron} from 'sentry/icons';
1012
import {t} from 'sentry/locale';
1113
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
@@ -16,17 +18,23 @@ import {FieldValueType} from 'sentry/utils/fields';
1618
import {useLocation} from 'sentry/utils/useLocation';
1719
import useOrganization from 'sentry/utils/useOrganization';
1820
import usePageFilters from 'sentry/utils/usePageFilters';
21+
import useProjects from 'sentry/utils/useProjects';
1922
import type {TableColumn} from 'sentry/views/discover/table/types';
2023
import {TimestampRenderer} from 'sentry/views/explore/logs/fieldRenderers';
2124
import {getLogColors} from 'sentry/views/explore/logs/styles';
2225
import {SeverityLevel} from 'sentry/views/explore/logs/utils';
26+
import {
27+
NoPaddingColumns,
28+
type AlwaysPresentTraceMetricFields,
29+
} from 'sentry/views/explore/metrics/constants';
2330
import {useTraceTelemetry} from 'sentry/views/explore/metrics/hooks/useTraceTelemetry';
2431
import {MetricDetails} from 'sentry/views/explore/metrics/metricInfoTabs/metricDetails';
2532
import {
2633
ExpandedRowContainer,
2734
NumericSimpleTableRowCell,
2835
StickyTableRow,
2936
StyledSimpleTableRowCell,
37+
TableRowContainer,
3038
WrappingText,
3139
} from 'sentry/views/explore/metrics/metricInfoTabs/metricInfoTabStyles';
3240
import {stripMetricParamsFromLocation} from 'sentry/views/explore/metrics/metricQuery';
@@ -69,7 +77,7 @@ function FieldCellWrapper({
6977
embedded?: boolean;
7078
}) {
7179
const columnType = getMetricTableColumnType(field);
72-
const hasPadding = field !== VirtualTableSampleColumnKey.EXPAND_ROW;
80+
const hasPadding = !NoPaddingColumns.includes(field as VirtualTableSampleColumnKey);
7381
if (columnType === 'stat') {
7482
return (
7583
<NumericSimpleTableRowCell
@@ -115,6 +123,11 @@ export function SampleTableRow({
115123
const theme = useTheme();
116124
const [isExpanded, setIsExpanded] = useState(false);
117125
const measureRef = useRef<HTMLTableRowElement>(null);
126+
const projects = useProjects();
127+
const projectId: (typeof AlwaysPresentTraceMetricFields)[1] =
128+
row[TraceMetricKnownFieldKey.PROJECT_ID];
129+
const project = projects.projects.find(p => p.id === '' + projectId);
130+
const projectSlug = project?.slug ?? '';
118131

119132
const traceId = row[TraceMetricKnownFieldKey.TRACE];
120133
const telemetry = telemetryData?.get?.(traceId);
@@ -137,9 +150,9 @@ export function SampleTableRow({
137150
const spanId = row[TraceMetricKnownFieldKey.SPAN_ID];
138151
const oldSpanId = row[TraceMetricKnownFieldKey.OLD_SPAN_ID] as string;
139152
const spanIdToUse = oldSpanId || spanId;
140-
const hasSpans = (telemetry?.spansCount ?? 0) > 0;
141153
const strippedLocation = stripMetricParamsFromLocation(location);
142154

155+
const hasSpans = (telemetry?.spansCount ?? 0) > 0;
143156
const shouldGoToSpans = spanIdToUse && hasSpans;
144157

145158
const target = getTraceDetailsUrl({
@@ -150,12 +163,12 @@ export function SampleTableRow({
150163
end: selection.datetime.end,
151164
statsPeriod: selection.datetime.period,
152165
},
153-
location: strippedLocation,
154166
timestamp,
155-
source: TraceViewSources.TRACES, // TODO: Should be TraceViewSources.TRACE_METRICS later after the trace view changes
167+
location: strippedLocation,
168+
source: TraceViewSources.TRACE_METRICS,
156169
spanId: shouldGoToSpans ? spanIdToUse : undefined,
157-
// tab: shouldGoToSpans ? TraceLayoutTabKeys.WATERFALL : TraceLayoutTabKeys.METRICS, // TODO: Add metrics tab to trace view
158-
tab: TraceLayoutTabKeys.WATERFALL,
170+
// tab: shouldGoToSpans ? TraceLayoutTabKeys.WATERFALL : TraceLayoutTabKeys.METRICS, // TODO: Can use this if want to go to the waterfall view if we add metrics to span details.
171+
tab: TraceLayoutTabKeys.METRICS,
159172
});
160173

161174
return (
@@ -263,6 +276,14 @@ export function SampleTableRow({
263276
);
264277
};
265278

279+
const renderProjectCell = () => {
280+
return (
281+
<Flex align="center" justify="center" style={{minWidth: '18px'}}>
282+
<ProjectBadge avatarSize={14} project={project ?? {slug: projectSlug}} hideName />
283+
</Flex>
284+
);
285+
};
286+
266287
const renderMap: Record<SampleTableColumnKey, () => ReactNode> = {
267288
[VirtualTableSampleColumnKey.EXPAND_ROW]: renderExpandRowCell,
268289
[TraceMetricKnownFieldKey.TRACE]: renderTraceCell,
@@ -271,6 +292,7 @@ export function SampleTableRow({
271292
[VirtualTableSampleColumnKey.LOGS]: renderLogsCell,
272293
[VirtualTableSampleColumnKey.SPANS]: renderSpansCell,
273294
[VirtualTableSampleColumnKey.ERRORS]: renderErrorsCell,
295+
[VirtualTableSampleColumnKey.PROJECT_BADGE]: renderProjectCell,
274296
[TraceMetricKnownFieldKey.METRIC_TYPE]: renderMetricTypeCell,
275297
};
276298

@@ -315,14 +337,3 @@ export function SampleTableRow({
315337
</TableRowContainer>
316338
);
317339
}
318-
319-
const TableRowContainer = styled('div')`
320-
display: grid;
321-
grid-template-columns: subgrid;
322-
grid-auto-rows: min-content;
323-
grid-column: 1 / -1;
324-
325-
:not(:last-child) {
326-
border-bottom: 1px solid ${p => p.theme.border};
327-
}
328-
`;

static/app/views/explore/metrics/metricsFlags.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import type {Organization} from 'sentry/types/organization';
55

6-
const canUseMetricsUI = (organization: Organization) => {
6+
export const canUseMetricsUI = (organization: Organization) => {
77
return organization.features.includes('tracemetrics-enabled');
88
};
99

static/app/views/explore/metrics/metricsFrozenContext.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import type {ReactNode} from 'react';
2+
import {useMemo} from 'react';
3+
4+
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
15
import type {DateString} from 'sentry/types/core';
26
import {createDefinedContext} from 'sentry/utils/performance/contexts/utils';
37
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
8+
import {TraceMetricKnownFieldKey} from 'sentry/views/explore/metrics/types';
49

5-
interface TracePeriod {
10+
export interface TracePeriod {
611
end?: DateString;
712
period?: string | null;
813
start?: DateString;
@@ -16,12 +21,38 @@ interface MetricsFrozenContextValue {
1621
tracePeriod?: TracePeriod;
1722
}
1823

19-
const [_MetricsFrozenContextProvider, _useMetricsFrozenContext] =
24+
const [_MetricsFrozenContextProvider, _useMetricsFrozenContext, MetricsFrozenContext] =
2025
createDefinedContext<MetricsFrozenContextValue>({
2126
name: 'MetricsFrozenContext',
2227
strict: false,
2328
});
2429

30+
export interface MetricsFrozenForTracesProviderProps {
31+
traceIds: string[];
32+
children?: ReactNode;
33+
tracePeriod?: TracePeriod;
34+
}
35+
36+
export function MetricsFrozenContextProvider(props: MetricsFrozenForTracesProviderProps) {
37+
const value: MetricsFrozenContextValue = useMemo(() => {
38+
if (props.traceIds.length) {
39+
const search = new MutableSearch('');
40+
const traceIds = `[${props.traceIds.join(',')}]`;
41+
search.addFilterValue(TraceMetricKnownFieldKey.TRACE, traceIds);
42+
return {
43+
frozen: true,
44+
search,
45+
traceIds: props.traceIds,
46+
projectIds: [ALL_ACCESS_PROJECTS],
47+
tracePeriod: props.tracePeriod,
48+
};
49+
}
50+
51+
return {frozen: false};
52+
}, [props]);
53+
54+
return <MetricsFrozenContext value={value}>{props.children}</MetricsFrozenContext>;
55+
}
2556
function useMetricsFrozenContext() {
2657
return _useMetricsFrozenContext() ?? {};
2758
}

0 commit comments

Comments
 (0)