Skip to content

Commit b9ebdd3

Browse files
committed
add operation breakdown column
1 parent dbe22f5 commit b9ebdd3

File tree

2 files changed

+155
-9
lines changed

2 files changed

+155
-9
lines changed

static/app/views/insights/types.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ export enum SpanFields {
129129
AI_TOTAL_COST = 'ai.total_cost',
130130
AI_TOTAL_TOKENS_USED = 'ai.total_tokens.used',
131131

132+
// Span Operation Breakdown fields
133+
SPANS_BROWSER = 'spans.browser',
134+
SPANS_DB = 'spans.db',
135+
SPANS_HTTP = 'spans.http',
136+
SPANS_RESOURCE = 'spans.resource',
137+
SPANS_UI = 'spans.ui',
138+
132139
// DB fields
133140
DB_SYSTEM = 'db.system', // TODO: this is a duplicate of `SPAN_SYSTEM`
134141

@@ -250,7 +257,12 @@ export type SpanNumberFields =
250257
| SpanFields.THREAD_ID
251258
| SpanFields.PROJECT_ID
252259
| SpanFields.TTID
253-
| SpanFields.TTFD;
260+
| SpanFields.TTFD
261+
| SpanFields.SPANS_BROWSER
262+
| SpanFields.SPANS_DB
263+
| SpanFields.SPANS_HTTP
264+
| SpanFields.SPANS_RESOURCE
265+
| SpanFields.SPANS_UI;
254266

255267
// TODO: Enforce that these fields all come from SpanFields
256268
// These fields should never be `null` when coming from the backend. This list

static/app/views/performance/eap/overviewSpansTable.tsx

Lines changed: 142 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import {Fragment} from 'react';
22
import {useTheme} from '@emotion/react';
3+
import styled from '@emotion/styled';
34
import type {Location} from 'history';
45

56
import {LinkButton} from '@sentry/scraps/button';
67
import {Link} from '@sentry/scraps/link';
8+
import {Tooltip} from '@sentry/scraps/tooltip';
79

10+
import {Duration} from 'sentry/components/duration';
811
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
912
import {Pagination, type CursorHandler} from 'sentry/components/pagination';
13+
import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
14+
import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
15+
import {QuestionTooltip} from 'sentry/components/questionTooltip';
1016
import {
1117
COL_WIDTH_UNDEFINED,
1218
GridEditable,
@@ -17,6 +23,11 @@ import {t, tct} from 'sentry/locale';
1723
import type {Organization} from 'sentry/types/organization';
1824
import type {EventsMetaType, EventView} from 'sentry/utils/discover/eventView';
1925
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
26+
import {
27+
getSpanOperationName,
28+
SPAN_OP_BREAKDOWN_FIELDS,
29+
} from 'sentry/utils/discover/fields';
30+
import {toPercent} from 'sentry/utils/number/toPercent';
2031
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
2132
import {projectSupportsReplay} from 'sentry/utils/replays/projectSupportsReplay';
2233
import type {Theme} from 'sentry/utils/theme';
@@ -36,6 +47,8 @@ import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
3647

3748
const LIMIT = 50;
3849

50+
const SPAN_OPS_BREAKDOWN_COLUMN_KEY = 'span_ops_breakdown.relative';
51+
3952
const BASE_FIELDS: SpanProperty[] = [
4053
'span_id',
4154
'user.id',
@@ -46,11 +59,17 @@ const BASE_FIELDS: SpanProperty[] = [
4659
'span.duration',
4760
'trace',
4861
'timestamp',
62+
'replayId',
4963
'profile.id',
5064
'profiler.id',
5165
'thread.id',
5266
'precise.start_ts',
5367
'precise.finish_ts',
68+
'spans.browser',
69+
'spans.db',
70+
'spans.http',
71+
'spans.resource',
72+
'spans.ui',
5473
];
5574

5675
type OverviewSpansColumn = GridColumnHeader<
@@ -62,12 +81,18 @@ type OverviewSpansColumn = GridColumnHeader<
6281
| 'timestamp'
6382
| 'replayId'
6483
| 'profile.id'
84+
| typeof SPAN_OPS_BREAKDOWN_COLUMN_KEY
6585
>;
6686

6787
const BASE_COLUMN_ORDER: OverviewSpansColumn[] = [
6888
{key: 'span_id', name: t('Span ID'), width: COL_WIDTH_UNDEFINED},
6989
{key: 'user.display', name: t('User'), width: COL_WIDTH_UNDEFINED},
7090
{key: 'request.method', name: t('HTTP Method'), width: COL_WIDTH_UNDEFINED},
91+
{
92+
key: SPAN_OPS_BREAKDOWN_COLUMN_KEY,
93+
name: t('Operation Duration'),
94+
width: COL_WIDTH_UNDEFINED,
95+
},
7196
{key: 'span.duration', name: t('Total Duration'), width: COL_WIDTH_UNDEFINED},
7297
{key: 'trace', name: t('Trace ID'), width: COL_WIDTH_UNDEFINED},
7398
{key: 'timestamp', name: t('Timestamp'), width: COL_WIDTH_UNDEFINED},
@@ -106,10 +131,6 @@ export function OverviewSpansTable({eventView, transactionName}: Props) {
106131
project !== undefined &&
107132
projectSupportsReplay(project);
108133

109-
const fields: SpanProperty[] = showReplayColumn
110-
? BASE_FIELDS.concat('replayId')
111-
: BASE_FIELDS;
112-
113134
const columnOrder: OverviewSpansColumn[] = [
114135
...BASE_COLUMN_ORDER,
115136
...(showReplayColumn ? [REPLAY_COLUMN] : []),
@@ -159,7 +180,7 @@ export function OverviewSpansTable({eventView, transactionName}: Props) {
159180
} = useSpans(
160181
{
161182
search: defaultQuery,
162-
fields,
183+
fields: BASE_FIELDS,
163184
sorts: [sort],
164185
limit: LIMIT,
165186
cursor,
@@ -193,13 +214,28 @@ export function OverviewSpansTable({eventView, transactionName}: Props) {
193214
columnOrder={columnOrder}
194215
columnSortBy={[{key: sort.field, order: sort.kind}]}
195216
grid={{
196-
renderHeadCell: column =>
197-
renderHeadCell({
217+
renderHeadCell: column => {
218+
if (column.key === SPAN_OPS_BREAKDOWN_COLUMN_KEY) {
219+
return (
220+
<Fragment>
221+
<span>{column.name}</span>
222+
<StyledQuestionTooltip
223+
size="xs"
224+
position="top"
225+
title={t(
226+
'Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.'
227+
)}
228+
/>
229+
</Fragment>
230+
);
231+
}
232+
return renderHeadCell({
198233
column,
199234
sort,
200235
location,
201236
sortParameterName: QueryParameterNames.SPANS_SORT,
202-
}),
237+
});
238+
},
203239
renderBodyCell: (column, row) =>
204240
renderBodyCell(column, row, meta, projectSlug, location, organization, theme),
205241
}}
@@ -303,6 +339,10 @@ function renderBodyCell(
303339
);
304340
}
305341

342+
if (column.key === SPAN_OPS_BREAKDOWN_COLUMN_KEY) {
343+
return renderOperationDurationCell(row, theme);
344+
}
345+
306346
if (!meta || !meta?.fields) {
307347
return row[column.key];
308348
}
@@ -318,3 +358,97 @@ function renderBodyCell(
318358

319359
return rendered;
320360
}
361+
362+
function renderOperationDurationCell(row: Record<string, any>, theme: Theme) {
363+
const sumOfSpanTime = SPAN_OP_BREAKDOWN_FIELDS.reduce(
364+
(prev, curr) =>
365+
curr in row && typeof row[curr] === 'number' ? prev + row[curr] : prev,
366+
0
367+
);
368+
const cumulativeSpanOpBreakdown = Math.max(sumOfSpanTime, row['span.duration'] ?? 0);
369+
370+
if (
371+
SPAN_OP_BREAKDOWN_FIELDS.every(
372+
field => !(field in row) || typeof row[field] !== 'number'
373+
) ||
374+
cumulativeSpanOpBreakdown === 0
375+
) {
376+
return (
377+
<Duration
378+
seconds={(row['span.duration'] ?? 0) / 1000}
379+
fixedDigits={2}
380+
abbreviation
381+
/>
382+
);
383+
}
384+
385+
let otherPercentage = 1;
386+
387+
return (
388+
<RelativeOpsBreakdown data-test-id="relative-ops-breakdown">
389+
{SPAN_OP_BREAKDOWN_FIELDS.map(field => {
390+
if (!(field in row) || typeof row[field] !== 'number') {
391+
return null;
392+
}
393+
394+
const operationName = getSpanOperationName(field) ?? 'op';
395+
const spanOpDuration = row[field];
396+
const widthPercentage = spanOpDuration / cumulativeSpanOpBreakdown;
397+
otherPercentage = otherPercentage - widthPercentage;
398+
if (widthPercentage === 0) {
399+
return null;
400+
}
401+
return (
402+
<div key={operationName} style={{width: toPercent(widthPercentage || 0)}}>
403+
<Tooltip
404+
title={
405+
<div>
406+
<div>{operationName}</div>
407+
<div>
408+
<Duration
409+
seconds={spanOpDuration / 1000}
410+
fixedDigits={2}
411+
abbreviation
412+
/>
413+
</div>
414+
</div>
415+
}
416+
containerDisplayMode="block"
417+
>
418+
<RectangleRelativeOpsBreakdown
419+
style={{
420+
backgroundColor: pickBarColor(operationName, theme),
421+
}}
422+
/>
423+
</Tooltip>
424+
</div>
425+
);
426+
})}
427+
<div key="other" style={{width: toPercent(otherPercentage || 0)}}>
428+
<Tooltip title={<div>{t('Other')}</div>} containerDisplayMode="block">
429+
<OtherRelativeOpsBreakdown />
430+
</Tooltip>
431+
</div>
432+
</RelativeOpsBreakdown>
433+
);
434+
}
435+
436+
const StyledQuestionTooltip = styled(QuestionTooltip)`
437+
position: relative;
438+
top: 1px;
439+
left: 4px;
440+
`;
441+
442+
const RelativeOpsBreakdown = styled('div')`
443+
position: relative;
444+
display: flex;
445+
`;
446+
447+
const RectangleRelativeOpsBreakdown = styled(RowRectangle)`
448+
position: relative;
449+
width: 100%;
450+
`;
451+
452+
const OtherRelativeOpsBreakdown = styled(RectangleRelativeOpsBreakdown)`
453+
background-color: ${p => p.theme.colors.gray100};
454+
`;

0 commit comments

Comments
 (0)