Skip to content

Commit 5293664

Browse files
authored
feat(apm): Display the breakdown of the span operations (#16062)
1 parent f011d9a commit 5293664

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

src/sentry/static/sentry/app/components/events/interfaces/spans/traceView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {isValidSpanID, generateRootSpan} from './utils';
1616
import TraceViewHeader from './header';
1717
import * as CursorGuideHandler from './cursorGuideHandler';
1818

19-
type TraceContextType = {
19+
export type TraceContextType = {
2020
op?: string;
2121
type?: 'trace';
2222
span_id?: string;

src/sentry/static/sentry/app/views/eventsV2/eventDetails/content.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import EventInterfaces from './eventInterfaces';
3131
import LinkedIssue from './linkedIssue';
3232
import DiscoverBreadcrumb from '../breadcrumb';
3333
import {SectionHeading} from '../styles';
34+
import EventBreakdown from './transaction/eventBreakdown';
3435

3536
const slugValidator = function(
3637
props: {[key: string]: any},
@@ -170,6 +171,7 @@ class EventDetailsContent extends AsyncComponent<Props, State> {
170171
/>
171172
</div>
172173
<div style={{gridColumn: '2/3', display: isSidebarVisible ? '' : 'none'}}>
174+
<EventBreakdown event={event} />
173175
<EventMetadata
174176
event={event}
175177
organization={organization}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import React from 'react';
2+
import get from 'lodash/get';
3+
4+
import {Event} from 'app/types';
5+
import {
6+
SentryTransactionEvent,
7+
SpanEntry,
8+
SpanType,
9+
} from 'app/components/events/interfaces/spans/types';
10+
import {TraceContextType} from 'app/components/events/interfaces/spans/traceView';
11+
12+
type OpStats = {percentage: number; totalDuration: number};
13+
14+
type EventBreakdownType = {
15+
ops: ({name: string} & OpStats)[];
16+
unknown: OpStats | undefined;
17+
};
18+
19+
type Props = {
20+
event: Event;
21+
};
22+
23+
class EventBreakdown extends React.Component<Props> {
24+
getTransactionEvent(): SentryTransactionEvent | undefined {
25+
const {event} = this.props;
26+
27+
if (event.type === 'transaction') {
28+
return event as SentryTransactionEvent;
29+
}
30+
31+
return undefined;
32+
}
33+
34+
generateStats(): EventBreakdownType {
35+
const event = this.getTransactionEvent();
36+
37+
if (!event) {
38+
return {
39+
ops: [],
40+
unknown: undefined,
41+
};
42+
}
43+
44+
const traceContext: TraceContextType | undefined = get(event, 'contexts.trace');
45+
46+
if (!traceContext) {
47+
return {
48+
ops: [],
49+
unknown: undefined,
50+
};
51+
}
52+
53+
const spanEntry: SpanEntry | undefined = event.entries.find(
54+
(entry: {type: string}) => entry.type === 'spans'
55+
);
56+
57+
const spans: SpanType[] = get(spanEntry, 'data', []);
58+
59+
// track stats on spans with no operation
60+
const spansWithNoOperation = {count: 0, totalDuration: 0};
61+
62+
type AggregateType = {
63+
[opname: string]: {
64+
totalDuration: number; // num of seconds
65+
};
66+
};
67+
68+
const totalDuration = Math.abs(event.endTimestamp - event.startTimestamp);
69+
70+
spans.push({
71+
op: traceContext.op,
72+
timestamp: event.endTimestamp,
73+
start_timestamp: event.startTimestamp,
74+
trace_id: traceContext.trace_id || '',
75+
span_id: traceContext.span_id || '',
76+
data: {},
77+
});
78+
79+
const aggregateByOp: AggregateType = spans.reduce(
80+
(aggregate: AggregateType, span: SpanType) => {
81+
const op = span.op;
82+
const duration = Math.abs(span.timestamp - span.start_timestamp);
83+
84+
if (typeof op !== 'string') {
85+
spansWithNoOperation.count += 1;
86+
spansWithNoOperation.totalDuration += duration;
87+
88+
return aggregate;
89+
}
90+
const opStats = aggregate[op];
91+
92+
if (!opStats) {
93+
aggregate[op] = {
94+
totalDuration: duration,
95+
};
96+
return aggregate;
97+
}
98+
99+
aggregate[op].totalDuration += duration;
100+
101+
return aggregate;
102+
},
103+
{}
104+
);
105+
106+
const ops = Object.keys(aggregateByOp).map(opName => {
107+
return {
108+
name: opName,
109+
percentage: aggregateByOp[opName].totalDuration / totalDuration,
110+
totalDuration: aggregateByOp[opName].totalDuration,
111+
};
112+
});
113+
114+
ops.sort((firstOp, secondOp) => {
115+
// sort in descending order based on total duration
116+
117+
if (firstOp.percentage === secondOp.percentage) {
118+
return 0;
119+
}
120+
121+
if (firstOp.percentage > secondOp.percentage) {
122+
return -1;
123+
}
124+
125+
return 1;
126+
});
127+
128+
return {
129+
// use the first 4 ops with the top total duration
130+
ops: ops.slice(0, 4),
131+
unknown:
132+
spansWithNoOperation.count > 0
133+
? {
134+
percentage: spansWithNoOperation.totalDuration / totalDuration,
135+
totalDuration: spansWithNoOperation.totalDuration,
136+
}
137+
: undefined,
138+
};
139+
}
140+
141+
render() {
142+
const event = this.getTransactionEvent();
143+
144+
if (!event) {
145+
return null;
146+
}
147+
148+
// TODO: Dora to take over
149+
// const results = this.generateCounts();
150+
151+
return null;
152+
}
153+
}
154+
155+
export default EventBreakdown;

0 commit comments

Comments
 (0)