@@ -13,13 +13,17 @@ import {space} from 'sentry/styles/space';
13
13
import { Event , Group , Project } from 'sentry/types' ;
14
14
import { Series } from 'sentry/types/echarts' ;
15
15
import { defined } from 'sentry/utils' ;
16
+ import { trackAnalytics } from 'sentry/utils/analytics' ;
16
17
import { tooltipFormatter } from 'sentry/utils/discover/charts' ;
17
18
import { Container , NumberContainer } from 'sentry/utils/discover/styles' ;
18
19
import { getDuration } from 'sentry/utils/formatters' ;
19
20
import { useProfileFunctions } from 'sentry/utils/profiling/hooks/useProfileFunctions' ;
20
21
import { useProfileTopEventsStats } from 'sentry/utils/profiling/hooks/useProfileTopEventsStats' ;
21
22
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' ;
23
27
import { MutableSearch } from 'sentry/utils/tokenizeSearch' ;
24
28
import useOrganization from 'sentry/utils/useOrganization' ;
25
29
@@ -36,6 +40,8 @@ export function EventAffectedTransactions({
36
40
const evidenceData = event . occurrence ?. evidenceData ;
37
41
const fingerprint = evidenceData ?. fingerprint ;
38
42
const breakpoint = evidenceData ?. breakpoint ;
43
+ const frameName = evidenceData ?. function ;
44
+ const framePackage = evidenceData ?. package || evidenceData ?. module ;
39
45
40
46
const isValid = defined ( fingerprint ) && defined ( breakpoint ) ;
41
47
@@ -64,6 +70,8 @@ export function EventAffectedTransactions({
64
70
< EventAffectedTransactionsInner
65
71
breakpoint = { breakpoint }
66
72
fingerprint = { fingerprint }
73
+ frameName = { frameName }
74
+ framePackage = { framePackage }
67
75
project = { project }
68
76
/>
69
77
) ;
@@ -74,12 +82,16 @@ const TRANSACTIONS_LIMIT = 5;
74
82
interface EventAffectedTransactionsInnerProps {
75
83
breakpoint : number ;
76
84
fingerprint : number ;
85
+ frameName : string ;
86
+ framePackage : string ;
77
87
project : Project ;
78
88
}
79
89
80
90
function EventAffectedTransactionsInner ( {
81
91
breakpoint,
82
92
fingerprint,
93
+ frameName,
94
+ framePackage,
83
95
project,
84
96
} : EventAffectedTransactionsInnerProps ) {
85
97
const organization = useOrganization ( ) ;
@@ -132,11 +144,38 @@ function EventAffectedTransactionsInner({
132
144
query : query ?? '' ,
133
145
enabled : defined ( query ) ,
134
146
others : false ,
135
- referrer : 'api.profiling.functions.regression.stats' , // TODO: update this
147
+ referrer : 'api.profiling.functions.regression.transaction- stats' ,
136
148
topEvents : TRANSACTIONS_LIMIT ,
137
149
yAxes : [ 'p95()' , 'worst()' ] ,
138
150
} ) ;
139
151
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
+
140
179
const timeseriesByTransaction : Record < string , Series > = useMemo ( ( ) => {
141
180
const allTimeseries : Record < string , Series > = { } ;
142
181
if ( ! defined ( functionStats . data ) ) {
@@ -161,7 +200,7 @@ function EventAffectedTransactionsInner({
161
200
value : data . values [ i ] ,
162
201
} ;
163
202
} ) ,
164
- seriesName : 'p95()' ,
203
+ seriesName : 'p95(function.duration )' ,
165
204
} ;
166
205
} ) ;
167
206
@@ -192,15 +231,77 @@ function EventAffectedTransactionsInner({
192
231
} ;
193
232
} , [ ] ) ;
194
233
234
+ function handleGoToProfile ( ) {
235
+ trackAnalytics ( 'profiling_views.go_to_flamegraph' , {
236
+ organization,
237
+ source : 'profiling.issue.function_regression.transactions' ,
238
+ } ) ;
239
+ }
240
+
195
241
return (
196
242
< EventDataSection type = "transactions-impacted" title = { t ( 'Transactions Impacted' ) } >
197
243
< ListContainer >
198
244
{ ( 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 ] ?? {
200
247
seriesName : 'p95()' ,
201
248
data : [ ] ,
202
249
} ;
203
250
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
+
204
305
const summaryTarget = generateProfileSummaryRouteWithQuery ( {
205
306
orgSlug : organization . slug ,
206
307
projectSlug : project . slug ,
@@ -237,15 +338,9 @@ function EventAffectedTransactionsInner({
237
338
position = "top"
238
339
>
239
340
< DurationChange >
240
- < PerformanceDuration
241
- nanoseconds = { transaction [ percentileBefore ] as number }
242
- abbreviation
243
- />
341
+ { before }
244
342
< IconArrow direction = "right" size = "xs" />
245
- < PerformanceDuration
246
- nanoseconds = { transaction [ percentileAfter ] as number }
247
- abbreviation
248
- />
343
+ { after }
249
344
</ DurationChange >
250
345
</ Tooltip >
251
346
</ NumberContainer >
@@ -257,6 +352,66 @@ function EventAffectedTransactionsInner({
257
352
) ;
258
353
}
259
354
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
+
260
415
const ListContainer = styled ( 'div' ) `
261
416
display: grid;
262
417
grid-template-columns: 1fr auto auto;
0 commit comments