@@ -14,6 +14,7 @@ import {
1414 GLOBAL_OBJ ,
1515 SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON ,
1616 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
17+ SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE ,
1718 SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
1819 TRACING_DEFAULTS ,
1920 addNonEnumerableProperty ,
@@ -36,10 +37,10 @@ import { DEBUG_BUILD } from '../debug-build';
3637import { WINDOW } from '../helpers' ;
3738import { registerBackgroundTabDetection } from './backgroundtab' ;
3839import { defaultRequestInstrumentationOptions , instrumentOutgoingRequests } from './request' ;
39- import type { PreviousTraceInfo } from './previousTrace' ;
4040import {
4141 addPreviousTraceSpanLink ,
4242 getPreviousTraceFromSessionStorage ,
43+ spanContextSampled ,
4344 storePreviousTraceInSessionStorage ,
4445} from './previousTrace' ;
4546
@@ -172,6 +173,23 @@ export interface BrowserTracingOptions {
172173 */
173174 linkPreviousTrace : 'in-memory' | 'session-storage' | 'off' ;
174175
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+
175193 /**
176194 * _experiments allows the user to send options to define how this integration works.
177195 *
@@ -206,6 +224,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
206224 enableLongAnimationFrame : true ,
207225 enableInp : true ,
208226 linkPreviousTrace : 'in-memory' ,
227+ sampleLinkedTracesConsistently : false ,
209228 _experiments : { } ,
210229 ...defaultRequestInstrumentationOptions ,
211230} ;
@@ -246,6 +265,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
246265 instrumentPageLoad,
247266 instrumentNavigation,
248267 linkPreviousTrace,
268+ sampleLinkedTracesConsistently,
249269 } = {
250270 ...DEFAULT_BROWSER_TRACING_OPTIONS ,
251271 ..._options ,
@@ -322,6 +342,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
322342 } ) ;
323343 } ,
324344 } ) ;
345+
325346 setActiveIdleSpan ( client , idleSpan ) ;
326347
327348 function emitFinish ( ) : void {
@@ -389,20 +410,67 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
389410 } ) ;
390411
391412 if ( linkPreviousTrace !== 'off' ) {
392- let inMemoryPreviousTraceInfo : PreviousTraceInfo | undefined = undefined ;
413+ const useSessionStorage = linkPreviousTrace === 'session-storage' ;
414+
415+ let inMemoryPreviousTraceInfo = useSessionStorage ? getPreviousTraceFromSessionStorage ( ) : undefined ;
393416
394417 client . on ( 'spanStart' , span => {
395418 if ( getRootSpan ( span ) !== span ) {
396419 return ;
397420 }
398421
399- if ( linkPreviousTrace === 'session-storage' ) {
400- const updatedPreviousTraceInfo = addPreviousTraceSpanLink ( getPreviousTraceFromSessionStorage ( ) , span ) ;
401- storePreviousTraceInSessionStorage ( updatedPreviousTraceInfo ) ;
402- } else {
403- inMemoryPreviousTraceInfo = addPreviousTraceSpanLink ( inMemoryPreviousTraceInfo , span ) ;
422+ const scope = getCurrentScope ( ) ;
423+ const oldPropagationContext = scope . getPropagationContext ( ) ;
424+ inMemoryPreviousTraceInfo = addPreviousTraceSpanLink ( inMemoryPreviousTraceInfo , span , oldPropagationContext ) ;
425+
426+ if ( useSessionStorage ) {
427+ storePreviousTraceInSessionStorage ( inMemoryPreviousTraceInfo ) ;
404428 }
405429 } ) ;
430+
431+ if ( sampleLinkedTracesConsistently ) {
432+ /*
433+ This is a massive hack I'm really not proud of:
434+
435+ When users opt into `sampleLinkedTracesConsistently`, we need to make sure that we "propagate"
436+ the previous trace's sample rate and rand to the current trace. This is necessary because otherwise, span
437+ metric extrapolation is off, as we'd be propagating a too high sample rate for the subsequent traces.
438+
439+ So therefore, we pretend that the previous trace was the parent trace of the newly started trace. To do that,
440+ we mutate the propagation context of the current trace and set the sample rate and sample rand of the previous trace.
441+ Timing-wise, it is fine because it happens before we even sample the root span.
442+
443+ @see https://github.com/getsentry/sentry-javascript/issues/15754
444+ */
445+ client . on ( 'beforeSampling' , mutableSamplingContextData => {
446+ if ( ! inMemoryPreviousTraceInfo ) {
447+ return ;
448+ }
449+
450+ const scope = getCurrentScope ( ) ;
451+ const currentPropagationContext = scope . getPropagationContext ( ) ;
452+
453+ scope . setPropagationContext ( {
454+ ...currentPropagationContext ,
455+ dsc : {
456+ ...currentPropagationContext . dsc ,
457+ // The fallback to 0 should never happen; this is rather to satisfy the types
458+ sample_rate : String ( inMemoryPreviousTraceInfo . sampleRate ?? 0 ) ,
459+ sampled : String ( spanContextSampled ( inMemoryPreviousTraceInfo . spanContext ) ) ,
460+ } ,
461+ sampleRand : inMemoryPreviousTraceInfo . sampleRand ,
462+ } ) ;
463+
464+ mutableSamplingContextData . parentSampled = spanContextSampled ( inMemoryPreviousTraceInfo . spanContext ) ;
465+ mutableSamplingContextData . parentSampleRate = inMemoryPreviousTraceInfo . sampleRate ;
466+
467+ mutableSamplingContextData . spanAttributes = {
468+ ...mutableSamplingContextData . spanAttributes ,
469+ // record an attribute that this span was "force-sampled", so that we can later check on this.
470+ [ SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE ] : inMemoryPreviousTraceInfo . sampleRate ,
471+ } ;
472+ } ) ;
473+ }
406474 }
407475
408476 if ( WINDOW . location ) {
0 commit comments