@@ -14,6 +14,7 @@ import {
14
14
GLOBAL_OBJ ,
15
15
SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ,
16
16
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
17
+ SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE ,
17
18
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
18
19
TRACING_DEFAULTS ,
19
20
addNonEnumerableProperty ,
@@ -36,10 +37,10 @@ import { DEBUG_BUILD } from '../debug-build';
36
37
import { WINDOW } from '../helpers' ;
37
38
import { registerBackgroundTabDetection } from './backgroundtab' ;
38
39
import { defaultRequestInstrumentationOptions , instrumentOutgoingRequests } from './request' ;
39
- import type { PreviousTraceInfo } from './previousTrace' ;
40
40
import {
41
41
addPreviousTraceSpanLink ,
42
42
getPreviousTraceFromSessionStorage ,
43
+ spanContextSampled ,
43
44
storePreviousTraceInSessionStorage ,
44
45
} from './previousTrace' ;
45
46
@@ -172,6 +173,23 @@ export interface BrowserTracingOptions {
172
173
*/
173
174
linkPreviousTrace : 'in-memory' | 'session-storage' | 'off' ;
174
175
176
+ /**
177
+ * If true, Sentry will consistently sample subsequent traces based on the
178
+ * sampling decision of the initial trace. For example, if the initial page
179
+ * load trace was sampled positively, all subsequent traces (e.g. navigations)
180
+ * are also sampled positively. In case the initial trace was sampled negatively,
181
+ * all subsequent traces are also sampled negatively.
182
+ *
183
+ * This option lets you get consistent, linked traces within a user journey
184
+ * while maintaining an overall quota based on your trace sampling settings.
185
+ *
186
+ * This option is only effective if {@link BrowserTracingOptions.linkPreviousTrace}
187
+ * is enabled (i.e. not set to `'off'`).
188
+ *
189
+ * @default `false` - this is an opt-in feature.
190
+ */
191
+ sampleLinkedTracesConsistently : boolean ;
192
+
175
193
/**
176
194
* _experiments allows the user to send options to define how this integration works.
177
195
*
@@ -213,6 +231,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
213
231
enableLongAnimationFrame : true ,
214
232
enableInp : true ,
215
233
linkPreviousTrace : 'in-memory' ,
234
+ sampleLinkedTracesConsistently : false ,
216
235
_experiments : { } ,
217
236
...defaultRequestInstrumentationOptions ,
218
237
} ;
@@ -253,6 +272,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
253
272
instrumentPageLoad,
254
273
instrumentNavigation,
255
274
linkPreviousTrace,
275
+ sampleLinkedTracesConsistently,
256
276
onRequestSpanStart,
257
277
} = {
258
278
...DEFAULT_BROWSER_TRACING_OPTIONS ,
@@ -330,6 +350,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
330
350
} ) ;
331
351
} ,
332
352
} ) ;
353
+
333
354
setActiveIdleSpan ( client , idleSpan ) ;
334
355
335
356
function emitFinish ( ) : void {
@@ -397,20 +418,67 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
397
418
} ) ;
398
419
399
420
if ( linkPreviousTrace !== 'off' ) {
400
- let inMemoryPreviousTraceInfo : PreviousTraceInfo | undefined = undefined ;
421
+ const useSessionStorage = linkPreviousTrace === 'session-storage' ;
422
+
423
+ let inMemoryPreviousTraceInfo = useSessionStorage ? getPreviousTraceFromSessionStorage ( ) : undefined ;
401
424
402
425
client . on ( 'spanStart' , span => {
403
426
if ( getRootSpan ( span ) !== span ) {
404
427
return ;
405
428
}
406
429
407
- if ( linkPreviousTrace === 'session-storage' ) {
408
- const updatedPreviousTraceInfo = addPreviousTraceSpanLink ( getPreviousTraceFromSessionStorage ( ) , span ) ;
409
- storePreviousTraceInSessionStorage ( updatedPreviousTraceInfo ) ;
410
- } else {
411
- inMemoryPreviousTraceInfo = addPreviousTraceSpanLink ( inMemoryPreviousTraceInfo , span ) ;
430
+ const scope = getCurrentScope ( ) ;
431
+ const oldPropagationContext = scope . getPropagationContext ( ) ;
432
+ inMemoryPreviousTraceInfo = addPreviousTraceSpanLink ( inMemoryPreviousTraceInfo , span , oldPropagationContext ) ;
433
+
434
+ if ( useSessionStorage ) {
435
+ storePreviousTraceInSessionStorage ( inMemoryPreviousTraceInfo ) ;
412
436
}
413
437
} ) ;
438
+
439
+ if ( sampleLinkedTracesConsistently ) {
440
+ /*
441
+ This is a massive hack I'm really not proud of:
442
+
443
+ When users opt into `sampleLinkedTracesConsistently`, we need to make sure that we "propagate"
444
+ the previous trace's sample rate and rand to the current trace. This is necessary because otherwise, span
445
+ metric extrapolation is off, as we'd be propagating a too high sample rate for the subsequent traces.
446
+
447
+ So therefore, we pretend that the previous trace was the parent trace of the newly started trace. To do that,
448
+ we mutate the propagation context of the current trace and set the sample rate and sample rand of the previous trace.
449
+ Timing-wise, it is fine because it happens before we even sample the root span.
450
+
451
+ @see https://github.com/getsentry/sentry-javascript/issues/15754
452
+ */
453
+ client . on ( 'beforeSampling' , mutableSamplingContextData => {
454
+ if ( ! inMemoryPreviousTraceInfo ) {
455
+ return ;
456
+ }
457
+
458
+ const scope = getCurrentScope ( ) ;
459
+ const currentPropagationContext = scope . getPropagationContext ( ) ;
460
+
461
+ scope . setPropagationContext ( {
462
+ ...currentPropagationContext ,
463
+ dsc : {
464
+ ...currentPropagationContext . dsc ,
465
+ // The fallback to 0 should never happen; this is rather to satisfy the types
466
+ sample_rate : String ( inMemoryPreviousTraceInfo . sampleRate ?? 0 ) ,
467
+ sampled : String ( spanContextSampled ( inMemoryPreviousTraceInfo . spanContext ) ) ,
468
+ } ,
469
+ sampleRand : inMemoryPreviousTraceInfo . sampleRand ,
470
+ } ) ;
471
+
472
+ mutableSamplingContextData . parentSampled = spanContextSampled ( inMemoryPreviousTraceInfo . spanContext ) ;
473
+ mutableSamplingContextData . parentSampleRate = inMemoryPreviousTraceInfo . sampleRate ;
474
+
475
+ mutableSamplingContextData . spanAttributes = {
476
+ ...mutableSamplingContextData . spanAttributes ,
477
+ // record an attribute that this span was "force-sampled", so that we can later check on this.
478
+ [ SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE ] : inMemoryPreviousTraceInfo . sampleRate ,
479
+ } ;
480
+ } ) ;
481
+ }
414
482
}
415
483
416
484
if ( WINDOW . location ) {
0 commit comments