Skip to content

Commit 1339e91

Browse files
authored
feat(statistical-detectors): Link transaction durations to example pr… (#58101)
…ofiles Use load the worst() timeseries and iterate before and after the breakpoint to find candidate profiles to link to.
1 parent 143bb38 commit 1339e91

File tree

5 files changed

+185
-41
lines changed

5 files changed

+185
-41
lines changed

static/app/components/events/eventStatisticalDetector/eventAffectedTransactions.tsx

Lines changed: 167 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ import {space} from 'sentry/styles/space';
1313
import {Event, Group, Project} from 'sentry/types';
1414
import {Series} from 'sentry/types/echarts';
1515
import {defined} from 'sentry/utils';
16+
import {trackAnalytics} from 'sentry/utils/analytics';
1617
import {tooltipFormatter} from 'sentry/utils/discover/charts';
1718
import {Container, NumberContainer} from 'sentry/utils/discover/styles';
1819
import {getDuration} from 'sentry/utils/formatters';
1920
import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
2021
import {useProfileTopEventsStats} from 'sentry/utils/profiling/hooks/useProfileTopEventsStats';
2122
import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime';
22-
import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes';
23+
import {
24+
generateProfileFlamechartRouteWithQuery,
25+
generateProfileSummaryRouteWithQuery,
26+
} from 'sentry/utils/profiling/routes';
2327
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
2428
import useOrganization from 'sentry/utils/useOrganization';
2529

@@ -36,6 +40,8 @@ export function EventAffectedTransactions({
3640
const evidenceData = event.occurrence?.evidenceData;
3741
const fingerprint = evidenceData?.fingerprint;
3842
const breakpoint = evidenceData?.breakpoint;
43+
const frameName = evidenceData?.function;
44+
const framePackage = evidenceData?.package || evidenceData?.module;
3945

4046
const isValid = defined(fingerprint) && defined(breakpoint);
4147

@@ -64,6 +70,8 @@ export function EventAffectedTransactions({
6470
<EventAffectedTransactionsInner
6571
breakpoint={breakpoint}
6672
fingerprint={fingerprint}
73+
frameName={frameName}
74+
framePackage={framePackage}
6775
project={project}
6876
/>
6977
);
@@ -74,12 +82,16 @@ const TRANSACTIONS_LIMIT = 5;
7482
interface EventAffectedTransactionsInnerProps {
7583
breakpoint: number;
7684
fingerprint: number;
85+
frameName: string;
86+
framePackage: string;
7787
project: Project;
7888
}
7989

8090
function EventAffectedTransactionsInner({
8191
breakpoint,
8292
fingerprint,
93+
frameName,
94+
framePackage,
8395
project,
8496
}: EventAffectedTransactionsInnerProps) {
8597
const organization = useOrganization();
@@ -132,11 +144,38 @@ function EventAffectedTransactionsInner({
132144
query: query ?? '',
133145
enabled: defined(query),
134146
others: false,
135-
referrer: 'api.profiling.functions.regression.stats', // TODO: update this
147+
referrer: 'api.profiling.functions.regression.transaction-stats',
136148
topEvents: TRANSACTIONS_LIMIT,
137149
yAxes: ['p95()', 'worst()'],
138150
});
139151

152+
const examplesByTransaction = useMemo(() => {
153+
const allExamples: Record<string, [string | null, string | null]> = {};
154+
if (!defined(functionStats.data)) {
155+
return allExamples;
156+
}
157+
158+
const timestamps = functionStats.data.timestamps;
159+
const breakpointIndex = timestamps.indexOf(breakpoint);
160+
if (breakpointIndex < 0) {
161+
return allExamples;
162+
}
163+
164+
transactionsDeltaQuery.data?.data?.forEach(row => {
165+
const transaction = row.transaction as string;
166+
const data = functionStats.data.data.find(
167+
({axis, label}) => axis === 'worst()' && label === transaction
168+
);
169+
if (!defined(data)) {
170+
return;
171+
}
172+
173+
allExamples[transaction] = findExamplePair(data.values, breakpointIndex);
174+
});
175+
176+
return allExamples;
177+
}, [breakpoint, transactionsDeltaQuery, functionStats]);
178+
140179
const timeseriesByTransaction: Record<string, Series> = useMemo(() => {
141180
const allTimeseries: Record<string, Series> = {};
142181
if (!defined(functionStats.data)) {
@@ -161,7 +200,7 @@ function EventAffectedTransactionsInner({
161200
value: data.values[i],
162201
};
163202
}),
164-
seriesName: 'p95()',
203+
seriesName: 'p95(function.duration)',
165204
};
166205
});
167206

@@ -192,15 +231,77 @@ function EventAffectedTransactionsInner({
192231
};
193232
}, []);
194233

234+
function handleGoToProfile() {
235+
trackAnalytics('profiling_views.go_to_flamegraph', {
236+
organization,
237+
source: 'profiling.issue.function_regression.transactions',
238+
});
239+
}
240+
195241
return (
196242
<EventDataSection type="transactions-impacted" title={t('Transactions Impacted')}>
197243
<ListContainer>
198244
{(transactionsDeltaQuery.data?.data ?? []).map(transaction => {
199-
const series = timeseriesByTransaction[transaction.transaction as string] ?? {
245+
const transactionName = transaction.transaction as string;
246+
const series = timeseriesByTransaction[transactionName] ?? {
200247
seriesName: 'p95()',
201248
data: [],
202249
};
203250

251+
const [beforeExample, afterExample] = examplesByTransaction[
252+
transactionName
253+
] ?? [null, null];
254+
255+
let before = (
256+
<PerformanceDuration
257+
nanoseconds={transaction[percentileBefore] as number}
258+
abbreviation
259+
/>
260+
);
261+
262+
if (defined(beforeExample)) {
263+
const beforeTarget = generateProfileFlamechartRouteWithQuery({
264+
orgSlug: organization.slug,
265+
projectSlug: project.slug,
266+
profileId: beforeExample,
267+
query: {
268+
frameName,
269+
framePackage,
270+
},
271+
});
272+
273+
before = (
274+
<Link to={beforeTarget} onClick={handleGoToProfile}>
275+
{before}
276+
</Link>
277+
);
278+
}
279+
280+
let after = (
281+
<PerformanceDuration
282+
nanoseconds={transaction[percentileAfter] as number}
283+
abbreviation
284+
/>
285+
);
286+
287+
if (defined(afterExample)) {
288+
const afterTarget = generateProfileFlamechartRouteWithQuery({
289+
orgSlug: organization.slug,
290+
projectSlug: project.slug,
291+
profileId: afterExample,
292+
query: {
293+
frameName,
294+
framePackage,
295+
},
296+
});
297+
298+
after = (
299+
<Link to={afterTarget} onClick={handleGoToProfile}>
300+
{after}
301+
</Link>
302+
);
303+
}
304+
204305
const summaryTarget = generateProfileSummaryRouteWithQuery({
205306
orgSlug: organization.slug,
206307
projectSlug: project.slug,
@@ -237,15 +338,9 @@ function EventAffectedTransactionsInner({
237338
position="top"
238339
>
239340
<DurationChange>
240-
<PerformanceDuration
241-
nanoseconds={transaction[percentileBefore] as number}
242-
abbreviation
243-
/>
341+
{before}
244342
<IconArrow direction="right" size="xs" />
245-
<PerformanceDuration
246-
nanoseconds={transaction[percentileAfter] as number}
247-
abbreviation
248-
/>
343+
{after}
249344
</DurationChange>
250345
</Tooltip>
251346
</NumberContainer>
@@ -257,6 +352,66 @@ function EventAffectedTransactionsInner({
257352
);
258353
}
259354

355+
/**
356+
* Find an example pair of profile ids from before and after the breakpoint.
357+
*
358+
* We prioritize profile ids from outside some window around the breakpoint
359+
* because the breakpoint is not 100% accurate and giving a buffer around
360+
* the breakpoint to so we can more accurate get a example profile from
361+
* before and after ranges.
362+
*
363+
* @param examples list of example profile ids
364+
* @param breakpointIndex the index where the breakpoint is
365+
* @param window the window around the breakpoint to deprioritize
366+
*/
367+
function findExamplePair(
368+
examples: string[],
369+
breakpointIndex,
370+
window = 3
371+
): [string | null, string | null] {
372+
let before: string | null = null;
373+
374+
for (let i = breakpointIndex - window; i < examples.length && i >= 0; i--) {
375+
if (examples[i]) {
376+
before = examples[i];
377+
break;
378+
}
379+
}
380+
381+
if (!defined(before)) {
382+
for (
383+
let i = breakpointIndex;
384+
i < examples.length && i > breakpointIndex - window;
385+
i--
386+
) {
387+
if (examples[i]) {
388+
before = examples[i];
389+
break;
390+
}
391+
}
392+
}
393+
394+
let after: string | null = null;
395+
396+
for (let i = breakpointIndex + window; i < examples.length; i++) {
397+
if (examples[i]) {
398+
after = examples[i];
399+
break;
400+
}
401+
}
402+
403+
if (!defined(before)) {
404+
for (let i = breakpointIndex; i < breakpointIndex + window; i++) {
405+
if (examples[i]) {
406+
after = examples[i];
407+
break;
408+
}
409+
}
410+
}
411+
412+
return [before, after];
413+
}
414+
260415
const ListContainer = styled('div')`
261416
display: grid;
262417
grid-template-columns: 1fr auto auto;

static/app/components/events/eventStatisticalDetector/eventFunctionComparisonList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ function EventList({
255255
onClick={() => {
256256
trackAnalytics('profiling_views.go_to_flamegraph', {
257257
organization,
258-
source: 'profiling.issue.function_regression',
258+
source: 'profiling.issue.function_regression.list',
259259
});
260260
}}
261261
>

static/app/utils/analytics/profilingAnalyticsEvents.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ type ProfilingEventSource =
1111
| 'profiling.function_trends.improvement'
1212
| 'profiling.function_trends.regression'
1313
| 'profiling.global_suspect_functions'
14-
| 'profiling.issue.function_regression'
14+
| 'profiling.issue.function_regression.list'
15+
| 'profiling.issue.function_regression.transactions'
1516
| 'profiling_transaction.suspect_functions_table'
1617
| 'profiling_transaction.slowest_functions_table'
1718
| 'profiling_transaction.regressed_functions_table'

static/app/utils/profiling/hooks/useProfileEventsStats.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,22 +148,21 @@ export function transformSingleSeries<F extends string>(
148148
dataset: 'discover' | 'profiles' | 'profileFunctions',
149149
yAxis: F,
150150
rawSeries: any,
151-
label?: string,
152-
formatter?: any
151+
label?: string
153152
) {
154-
if (!defined(formatter)) {
155-
const type =
156-
rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)];
157-
formatter =
158-
type === 'duration'
159-
? makeFormatTo(
160-
rawSeries.meta.units[yAxis] ??
161-
rawSeries.meta.units[getAggregateAlias(yAxis)] ??
162-
'nanoseconds',
163-
'milliseconds'
164-
)
165-
: value => value;
166-
}
153+
const type =
154+
rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)];
155+
const formatter =
156+
type === 'duration'
157+
? makeFormatTo(
158+
rawSeries.meta.units[yAxis] ??
159+
rawSeries.meta.units[getAggregateAlias(yAxis)] ??
160+
'nanoseconds',
161+
'milliseconds'
162+
)
163+
: type === 'string'
164+
? value => value || ''
165+
: value => value;
167166

168167
const series: EventsStatsSeries<F>['data'][number] = {
169168
axis: yAxis,

static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte
44
import {EventsStatsSeries, PageFilters} from 'sentry/types';
55
import {defined} from 'sentry/utils';
66
import {transformSingleSeries} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
7-
import {makeFormatTo} from 'sentry/utils/profiling/units/units';
87
import {useApiQuery} from 'sentry/utils/queryClient';
98
import useOrganization from 'sentry/utils/useOrganization';
109
import usePageFilters from 'sentry/utils/usePageFilters';
@@ -99,10 +98,6 @@ function transformTopEventsStatsResponse<F extends string>(
9998

10099
let firstSeries = true;
101100

102-
// TODO: the formatter should be inferred but the response does
103-
// not contain the meta at this time
104-
const formatter = makeFormatTo('nanoseconds', 'milliseconds');
105-
106101
for (const label of Object.keys(rawData)) {
107102
for (const yAxis of yAxes) {
108103
let dataForYAxis = rawData[label];
@@ -113,13 +108,7 @@ function transformTopEventsStatsResponse<F extends string>(
113108
continue;
114109
}
115110

116-
const transformed = transformSingleSeries(
117-
dataset,
118-
yAxis,
119-
dataForYAxis,
120-
label,
121-
formatter
122-
);
111+
const transformed = transformSingleSeries(dataset, yAxis, dataForYAxis, label);
123112

124113
if (firstSeries) {
125114
meta = transformed.meta;

0 commit comments

Comments
 (0)