Skip to content

Commit e20807e

Browse files
gggritsoc298lee
authored andcommitted
feat(perf): Add HTTP code breakdown chart to sample panel (#68652)
Fancy new chart, fancy new selector. See a full breakdown of _all_ HTTP codes within the selected range!
1 parent f66fde3 commit e20807e

File tree

5 files changed

+168
-44
lines changed

5 files changed

+168
-44
lines changed

static/app/utils/tokenizeSearch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {escapeDoubleQuotes} from 'sentry/utils';
22

3-
const ALLOWED_WILDCARD_FIELDS = ['span.description'];
3+
const ALLOWED_WILDCARD_FIELDS = ['span.description', 'span.status_code'];
44
export const EMPTY_OPTION_VALUE = '(empty)' as const;
55

66
export enum TokenType {

static/app/views/performance/http/httpSamplesPanel.spec.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ describe('HTTPSamplesPanel', () => {
111111
transaction: '/api/0/users',
112112
transactionMethod: 'GET',
113113
panel: 'status',
114+
responseCodeClass: '3',
114115
},
115116
hash: '',
116117
state: undefined,
@@ -127,12 +128,18 @@ describe('HTTPSamplesPanel', () => {
127128
}),
128129
],
129130
body: {
130-
'spm()': {
131+
'301': {
131132
data: [
132133
[1699907700, [{count: 7810.2}]],
133134
[1699908000, [{count: 1216.8}]],
134135
],
135136
},
137+
'304': {
138+
data: [
139+
[1699907700, [{count: 2701.5}]],
140+
[1699908000, [{count: 78.12}]],
141+
],
142+
},
136143
},
137144
});
138145
});
@@ -177,22 +184,18 @@ describe('HTTPSamplesPanel', () => {
177184
dataset: 'spansMetrics',
178185
environment: [],
179186
excludeOther: 0,
180-
field: [],
187+
field: ['span.status_code', 'count()'],
181188
interval: '30m',
182189
orderby: undefined,
183190
partial: 1,
184191
per_page: 50,
185192
project: [],
186193
query:
187-
'span.module:http span.domain:"\\*.sentry.dev" transaction:/api/0/users',
194+
'span.module:http span.domain:"\\*.sentry.dev" transaction:/api/0/users span.status_code:[300,301,302,303,304,305,307,308]',
188195
referrer: 'api.starfish.http-module-samples-panel-response-code-chart',
189196
statsPeriod: '10d',
190-
topEvents: undefined,
191-
yAxis: [
192-
'http_response_rate(3)',
193-
'http_response_rate(4)',
194-
'http_response_rate(5)',
195-
],
197+
topEvents: '5',
198+
yAxis: 'count()',
196199
},
197200
})
198201
);

static/app/views/performance/http/httpSamplesPanel.tsx

Lines changed: 99 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as qs from 'query-string';
55

66
import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
77
import {Button} from 'sentry/components/button';
8+
import {CompactSelect} from 'sentry/components/compactSelect';
89
import {SegmentedControl} from 'sentry/components/segmentedControl';
910
import {t} from 'sentry/locale';
1011
import {space} from 'sentry/styles/space';
@@ -19,9 +20,11 @@ import useProjects from 'sentry/utils/useProjects';
1920
import useRouter from 'sentry/utils/useRouter';
2021
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
2122
import {AverageValueMarkLine} from 'sentry/views/performance/charts/averageValueMarkLine';
23+
import {HTTP_RESPONSE_STATUS_CODES} from 'sentry/views/performance/http/definitions';
2224
import {DurationChart} from 'sentry/views/performance/http/durationChart';
2325
import decodePanel from 'sentry/views/performance/http/queryParameterDecoders/panel';
24-
import {ResponseRateChart} from 'sentry/views/performance/http/responseRateChart';
26+
import decodeResponseCodeClass from 'sentry/views/performance/http/queryParameterDecoders/responseCodeClass';
27+
import {ResponseCodeCountChart} from 'sentry/views/performance/http/responseCodeCountChart';
2528
import {SpanSamplesTable} from 'sentry/views/performance/http/spanSamplesTable';
2629
import {useDebouncedState} from 'sentry/views/performance/http/useDebouncedState';
2730
import {useSpanSamples} from 'sentry/views/performance/http/useSpanSamples';
@@ -32,6 +35,7 @@ import DetailPanel from 'sentry/views/starfish/components/detailPanel';
3235
import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
3336
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
3437
import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
38+
import {useSpanMetricsTopNSeries} from 'sentry/views/starfish/queries/useSpanMetricsTopNSeries';
3539
import {
3640
ModuleName,
3741
SpanFunction,
@@ -53,6 +57,7 @@ export function HTTPSamplesPanel() {
5357
transaction: decodeScalar,
5458
transactionMethod: decodeScalar,
5559
panel: decodePanel,
60+
responseCodeClass: decodeResponseCodeClass,
5661
},
5762
});
5863

@@ -86,19 +91,50 @@ export function HTTPSamplesPanel() {
8691
});
8792
};
8893

94+
const handleResponseCodeClassChange = newResponseCodeClass => {
95+
router.replace({
96+
pathname: location.pathname,
97+
query: {
98+
...location.query,
99+
responseCodeClass: newResponseCodeClass.value,
100+
},
101+
});
102+
};
103+
89104
const isPanelOpen = Boolean(detailKey);
90105

106+
// The ribbon is above the data selectors, and not affected by them. So, it has its own filters.
107+
const ribbonFilters: SpanMetricsQueryFilters = {
108+
'span.module': ModuleName.HTTP,
109+
'span.domain': query.domain,
110+
transaction: query.transaction,
111+
};
112+
113+
// These filters are for the charts and samples tables
91114
const filters: SpanMetricsQueryFilters = {
92115
'span.module': ModuleName.HTTP,
93116
'span.domain': query.domain,
94117
transaction: query.transaction,
95118
};
96119

120+
const responseCodeInRange = query.responseCodeClass
121+
? Object.keys(HTTP_RESPONSE_STATUS_CODES).filter(code =>
122+
code.startsWith(query.responseCodeClass)
123+
)
124+
: [];
125+
126+
if (responseCodeInRange.length > 0) {
127+
// TODO: Allow automatic array parameter concatenation
128+
filters['span.status_code'] = `[${responseCodeInRange.join(',')}]`;
129+
}
130+
131+
const search = MutableSearch.fromQueryObject(filters);
132+
97133
const {
98134
data: domainTransactionMetrics,
99135
isFetching: areDomainTransactionMetricsFetching,
100136
} = useSpanMetrics({
101-
search: MutableSearch.fromQueryObject(filters),
137+
search: MutableSearch.fromQueryObject(ribbonFilters),
102138
fields: [
103139
`${SpanFunction.SPM}()`,
104140
`avg(${SpanMetricsField.SPAN_SELF_TIME})`,
@@ -117,7 +153,7 @@ export function HTTPSamplesPanel() {
117153
data: durationData,
118154
error: durationError,
119155
} = useSpanMetricsSeries({
120-
search: MutableSearch.fromQueryObject(filters),
156+
search,
121157
yAxis: [`avg(span.self_time)`],
122158
enabled: isPanelOpen && query.panel === 'duration',
123159
referrer: 'api.starfish.http-module-samples-panel-duration-chart',
@@ -127,9 +163,11 @@ export function HTTPSamplesPanel() {
127163
isFetching: isResponseCodeDataLoading,
128164
data: responseCodeData,
129165
error: responseCodeError,
130-
} = useSpanMetricsSeries({
131-
search: MutableSearch.fromQueryObject(filters),
132-
yAxis: ['http_response_rate(3)', 'http_response_rate(4)', 'http_response_rate(5)'],
166+
} = useSpanMetricsTopNSeries({
167+
search,
168+
fields: ['span.status_code', 'count()'],
169+
yAxis: ['count()'],
170+
topEvents: 5,
133171
enabled: isPanelOpen && query.panel === 'status',
134172
referrer: 'api.starfish.http-module-samples-panel-response-code-chart',
135173
});
@@ -142,7 +180,7 @@ export function HTTPSamplesPanel() {
142180
error: samplesDataError,
143181
refetch: refetchSpanSamples,
144182
} = useSpanSamples({
145-
search: MutableSearch.fromQueryObject(filters),
183+
search,
146184
fields: [
147185
SpanIndexedField.TRANSACTION_ID,
148186
SpanIndexedField.SPAN_DESCRIPTION,
@@ -276,18 +314,29 @@ export function HTTPSamplesPanel() {
276314
</ModuleLayout.Full>
277315

278316
<ModuleLayout.Full>
279-
<SegmentedControl
280-
value={query.panel}
281-
onChange={handlePanelChange}
282-
aria-label={t('Choose breakdown type')}
283-
>
284-
<SegmentedControl.Item key="duration">
285-
{t('By Duration')}
286-
</SegmentedControl.Item>
287-
<SegmentedControl.Item key="status">
288-
{t('By Response Code')}
289-
</SegmentedControl.Item>
290-
</SegmentedControl>
317+
<PanelControls>
318+
<SegmentedControl
319+
value={query.panel}
320+
onChange={handlePanelChange}
321+
aria-label={t('Choose breakdown type')}
322+
>
323+
<SegmentedControl.Item key="duration">
324+
{t('By Duration')}
325+
</SegmentedControl.Item>
326+
<SegmentedControl.Item key="status">
327+
{t('By Response Code')}
328+
</SegmentedControl.Item>
329+
</SegmentedControl>
330+
331+
<CompactSelect
332+
value={query.responseCodeClass}
333+
options={HTTP_RESPONSE_CODE_CLASS_OPTIONS}
334+
onChange={handleResponseCodeClassChange}
335+
triggerProps={{
336+
prefix: t('Response Code'),
337+
}}
338+
/>
339+
</PanelControls>
291340
</ModuleLayout.Full>
292341

293342
{query.panel === 'duration' && (
@@ -339,21 +388,8 @@ export function HTTPSamplesPanel() {
339388

340389
{query.panel === 'status' && (
341390
<ModuleLayout.Full>
342-
<ResponseRateChart
343-
series={[
344-
{
345-
...responseCodeData[`http_response_rate(3)`],
346-
seriesName: t('3XX'),
347-
},
348-
{
349-
...responseCodeData[`http_response_rate(4)`],
350-
seriesName: t('4XX'),
351-
},
352-
{
353-
...responseCodeData[`http_response_rate(5)`],
354-
seriesName: t('5XX'),
355-
},
356-
]}
391+
<ResponseCodeCountChart
392+
series={Object.values(responseCodeData).filter(Boolean)}
357393
isLoading={isResponseCodeDataLoading}
358394
error={responseCodeError}
359395
/>
@@ -377,6 +413,29 @@ const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
377413
padding-right: ${space(1)};
378414
`;
379415

416+
const HTTP_RESPONSE_CODE_CLASS_OPTIONS = [
417+
{
418+
value: '',
419+
label: t('All'),
420+
},
421+
{
422+
value: '2',
423+
label: t('2XXs'),
424+
},
425+
{
426+
value: '3',
427+
label: t('3XXs'),
428+
},
429+
{
430+
value: '4',
431+
label: t('4XXs'),
432+
},
433+
{
434+
value: '5',
435+
label: t('5XXs'),
436+
},
437+
];
438+
380439
const HeaderContainer = styled('div')`
381440
display: grid;
382441
grid-template-rows: auto auto auto;
@@ -404,3 +463,9 @@ const MetricsRibbon = styled('div')`
404463
flex-wrap: wrap;
405464
gap: ${space(4)};
406465
`;
466+
467+
const PanelControls = styled('div')`
468+
display: flex;
469+
justify-content: space-between;
470+
gap: ${space(2)};
471+
`;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {decodeScalar} from 'sentry/utils/queryString';
2+
3+
const OPTIONS = ['' as const, '2' as const, '3' as const, '4' as const, '5' as const];
4+
const DEFAULT = '';
5+
6+
type ResponseCodeClass = (typeof OPTIONS)[number];
7+
8+
export default function decode(
9+
value: string | string[] | undefined | null
10+
): ResponseCodeClass {
11+
const decodedValue = decodeScalar(value, DEFAULT);
12+
13+
if (isAValidOption(decodedValue)) {
14+
return decodedValue;
15+
}
16+
17+
return DEFAULT;
18+
}
19+
20+
function isAValidOption(maybeOption: string): maybeOption is ResponseCodeClass {
21+
// Manually widen to allow the comparison to string
22+
return (OPTIONS as unknown as string[]).includes(maybeOption as ResponseCodeClass);
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {t} from 'sentry/locale';
2+
import type {Series} from 'sentry/types/echarts';
3+
import {CHART_HEIGHT} from 'sentry/views/performance/database/settings';
4+
import Chart, {ChartType} from 'sentry/views/starfish/components/chart';
5+
import ChartPanel from 'sentry/views/starfish/components/chartPanel';
6+
7+
interface Props {
8+
isLoading: boolean;
9+
series: Series[];
10+
error?: Error | null;
11+
}
12+
13+
export function ResponseCodeCountChart({series, isLoading, error}: Props) {
14+
return (
15+
<ChartPanel title={t('Response Codes')}>
16+
<Chart
17+
showLegend
18+
height={CHART_HEIGHT}
19+
grid={{
20+
left: '4px',
21+
right: '0',
22+
top: '8px',
23+
bottom: '0',
24+
}}
25+
data={series}
26+
loading={isLoading}
27+
error={error}
28+
type={ChartType.LINE}
29+
aggregateOutputFormat="number"
30+
/>
31+
</ChartPanel>
32+
);
33+
}

0 commit comments

Comments
 (0)