From 7154f69db20a39f3db337d5d5262ee15172856bc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 11 Apr 2025 15:23:49 +0200 Subject: [PATCH 1/7] feat(browser): Add option to sample linked traces consistently --- .../utils/helpers.ts | 27 ++++++ .../src/tracing/browserTracingIntegration.ts | 81 +++++++++++++++-- packages/browser/src/tracing/previousTrace.ts | 57 +++++++++--- .../test/tracing/previousTrace.test.ts | 87 +++++++++++++++++-- packages/core/src/client.ts | 2 + packages/core/src/semanticAttributes.ts | 8 ++ .../src/tracing/dynamicSamplingContext.ts | 12 ++- packages/core/src/tracing/trace.ts | 18 +++- 8 files changed, 264 insertions(+), 28 deletions(-) diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index feecd7c5ce09..3f3863bd688d 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -1,5 +1,7 @@ +/* eslint-disable max-lines */ import type { Page, Request } from '@playwright/test'; import type { + ClientReport, Envelope, EnvelopeItem, EnvelopeItemType, @@ -254,6 +256,31 @@ export function waitForTransactionRequest( }); } +export function waitForClientReportRequest(page: Page, callback?: (report: ClientReport) => boolean): Promise { + return page.waitForRequest(req => { + const postData = req.postData(); + if (!postData) { + return false; + } + + try { + const maybeReport = envelopeRequestParser>(req); + + if (typeof maybeReport.discarded_events !== 'object') { + return false; + } + + if (callback) { + return callback(maybeReport as ClientReport); + } + + return true; + } catch { + return false; + } + }); +} + export async function waitForSession(page: Page): Promise { const req = await page.waitForRequest(req => { const postData = req.postData(); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 92b7ddcea364..8005d104ebb8 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -17,6 +17,7 @@ import { registerSpanErrorInstrumentation, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanIsSampled, spanToJSON, @@ -40,6 +41,7 @@ import type { PreviousTraceInfo } from './previousTrace'; import { addPreviousTraceSpanLink, getPreviousTraceFromSessionStorage, + spanContextSampled, storePreviousTraceInSessionStorage, } from './previousTrace'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; @@ -173,6 +175,23 @@ export interface BrowserTracingOptions { */ linkPreviousTrace: 'in-memory' | 'session-storage' | 'off'; + /** + * If true, Sentry will consistently sample subsequent traces based on the + * sampling decision of the initial trace. For example, if the initial page + * load trace was sampled positively, all subsequent traces (e.g. navigations) + * are also sampled positively. In case the initial trace was sampled negatively, + * all subsequent traces are also sampled negatively. + * + * This option lets you get consistent, linked traces within a user journey + * while maintaining an overall quota based on your trace sampling settings. + * + * This option is only effective if {@link BrowserTracingOptions.linkPreviousTrace} + * is enabled (i.e. not set to `'off'`). + * + * @default `false` - this is an opt-in feature. + */ + sampleLinkedTracesConsistently: boolean; + /** * _experiments allows the user to send options to define how this integration works. * @@ -214,6 +233,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, linkPreviousTrace: 'in-memory', + sampleLinkedTracesConsistently: false, _experiments: {}, ...defaultRequestInstrumentationOptions, }; @@ -265,6 +285,7 @@ export const browserTracingIntegration = ((_options: Partial { if (getRootSpan(span) !== span) { return; } - if (linkPreviousTrace === 'session-storage') { - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(getPreviousTraceFromSessionStorage(), span); - storePreviousTraceInSessionStorage(updatedPreviousTraceInfo); - } else { - inMemoryPreviousTraceInfo = addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span); + const scope = getCurrentScope(); + const oldPropagationContext = scope.getPropagationContext(); + inMemoryPreviousTraceInfo = addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span, oldPropagationContext); + + if (useSessionStorage) { + storePreviousTraceInSessionStorage(inMemoryPreviousTraceInfo); } }); + + if (sampleLinkedTracesConsistently) { + /* + This is a massive hack I'm really not proud of: + + When users opt into `sampleLinkedTracesConsistently`, we need to make sure that we "propagate" + the previous trace's sample rate and rand to the current trace. This is necessary because otherwise, span + metric extrapolation is off, as we'd be propagating a too high sample rate for the subsequent traces. + + So therefore, we pretend that the previous trace was the parent trace of the newly started trace. To do that, + we mutate the propagation context of the current trace and set the sample rate and sample rand of the previous trace. + Timing-wise, it is fine because it happens before we even sample the root span. + + @see https://github.com/getsentry/sentry-javascript/issues/15754 + */ + client.on('beforeSampling', mutableSamplingContextData => { + if (!inMemoryPreviousTraceInfo) { + return; + } + + const scope = getCurrentScope(); + const currentPropagationContext = scope.getPropagationContext(); + + scope.setPropagationContext({ + ...currentPropagationContext, + dsc: { + ...currentPropagationContext.dsc, + // The fallback to 0 should never happen; this is rather to satisfy the types + sample_rate: String(inMemoryPreviousTraceInfo.sampleRate ?? 0), + sampled: String(spanContextSampled(inMemoryPreviousTraceInfo.spanContext)), + }, + sampleRand: inMemoryPreviousTraceInfo.sampleRand, + }); + + mutableSamplingContextData.parentSampled = spanContextSampled(inMemoryPreviousTraceInfo.spanContext); + mutableSamplingContextData.parentSampleRate = inMemoryPreviousTraceInfo.sampleRate; + + mutableSamplingContextData.spanAttributes = { + ...mutableSamplingContextData.spanAttributes, + // record an attribute that this span was "force-sampled", so that we can later check on this. + [SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]: inMemoryPreviousTraceInfo.sampleRate, + }; + }); + } } if (WINDOW.location) { diff --git a/packages/browser/src/tracing/previousTrace.ts b/packages/browser/src/tracing/previousTrace.ts index 6d53833d718d..443c415d869d 100644 --- a/packages/browser/src/tracing/previousTrace.ts +++ b/packages/browser/src/tracing/previousTrace.ts @@ -1,5 +1,11 @@ -import type { Span } from '@sentry/core'; -import { type SpanContextData, logger, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, spanToJSON } from '@sentry/core'; +import type { PropagationContext, Span } from '@sentry/core'; +import { + type SpanContextData, + logger, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, + spanToJSON, +} from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../exports'; @@ -13,6 +19,16 @@ export interface PreviousTraceInfo { * Timestamp in seconds when the previous trace was started */ startTimestamp: number; + + /** + * sample rate of the previous trace + */ + sampleRate: number; + + /** + * The sample rand of the previous trace + */ + sampleRand: number; } // 1h in seconds @@ -33,14 +49,29 @@ export const PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE = 'sentry.previous_trace'; export function addPreviousTraceSpanLink( previousTraceInfo: PreviousTraceInfo | undefined, span: Span, + oldPropagationContext: PropagationContext, ): PreviousTraceInfo { const spanJson = spanToJSON(span); + function getSampleRate(): number { + try { + return ( + Number(oldPropagationContext.dsc?.sample_rate) ?? Number(spanJson.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]) + ); + } catch { + return 0; + } + } + + const updatedPreviousTraceInfo = { + spanContext: span.spanContext(), + startTimestamp: spanJson.start_timestamp, + sampleRate: getSampleRate(), + sampleRand: oldPropagationContext.sampleRand, + }; + if (!previousTraceInfo) { - return { - spanContext: span.spanContext(), - startTimestamp: spanJson.start_timestamp, - }; + return updatedPreviousTraceInfo; } const previousTraceSpanCtx = previousTraceInfo.spanContext; @@ -80,15 +111,12 @@ export function addPreviousTraceSpanLink( span.setAttribute( PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, `${previousTraceSpanCtx.traceId}-${previousTraceSpanCtx.spanId}-${ - previousTraceSpanCtx.traceFlags === 0x1 ? 1 : 0 + spanContextSampled(previousTraceSpanCtx) ? 1 : 0 }`, ); } - return { - spanContext: span.spanContext(), - startTimestamp: spanToJSON(span).start_timestamp, - }; + return updatedPreviousTraceInfo; } /** @@ -115,3 +143,10 @@ export function getPreviousTraceFromSessionStorage(): PreviousTraceInfo | undefi return undefined; } } + +/** + * see {@link import('@sentry/core').spanIsSampled} + */ +export const spanContextSampled = (ctx: SpanContextData): boolean => { + return ctx.traceFlags === 0x1; +}; diff --git a/packages/browser/test/tracing/previousTrace.test.ts b/packages/browser/test/tracing/previousTrace.test.ts index f11e2f0d67e5..550c48ebe013 100644 --- a/packages/browser/test/tracing/previousTrace.test.ts +++ b/packages/browser/test/tracing/previousTrace.test.ts @@ -7,6 +7,7 @@ import { PREVIOUS_TRACE_KEY, PREVIOUS_TRACE_MAX_DURATION, PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, + spanContextSampled, storePreviousTraceInSessionStorage, } from '../../src/tracing/previousTrace'; @@ -22,6 +23,8 @@ describe('addPreviousTraceSpanLink', () => { }, // max time reached almost exactly startTimestamp: currentSpanStart - PREVIOUS_TRACE_MAX_DURATION + 1, + sampleRand: 0.0126, + sampleRate: 0.5, }; const currentSpan = new SentrySpan({ @@ -33,7 +36,14 @@ describe('addPreviousTraceSpanLink', () => { sampled: true, }); - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: true, + dsc: { sample_rand: '0.0126', sample_rate: '0.5' }, + }; + + const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan, oldPropagationContext); const spanJson = spanToJSON(currentSpan); @@ -55,6 +65,8 @@ describe('addPreviousTraceSpanLink', () => { expect(updatedPreviousTraceInfo).toEqual({ spanContext: currentSpan.spanContext(), startTimestamp: currentSpanStart, + sampleRand: 0.0126, + sampleRate: 0.5, }); }); @@ -68,6 +80,8 @@ describe('addPreviousTraceSpanLink', () => { traceFlags: 0, }, startTimestamp: Date.now() / 1000 - PREVIOUS_TRACE_MAX_DURATION - 1, + sampleRand: 0.0126, + sampleRate: 0.5, }; const currentSpan = new SentrySpan({ @@ -75,7 +89,14 @@ describe('addPreviousTraceSpanLink', () => { startTimestamp: currentSpanStart, }); - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: true, + dsc: { sample_rand: '0.0126', sample_rate: '0.5' }, + }; + + const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan, oldPropagationContext); const spanJson = spanToJSON(currentSpan); @@ -87,6 +108,8 @@ describe('addPreviousTraceSpanLink', () => { expect(updatedPreviousTraceInfo).toEqual({ spanContext: currentSpan.spanContext(), startTimestamp: currentSpanStart, + sampleRand: 0.0126, + sampleRate: 0.5, }); }); @@ -98,6 +121,15 @@ describe('addPreviousTraceSpanLink', () => { traceFlags: 1, }, startTimestamp: Date.now() / 1000, + sampleRand: 0.0126, + sampleRate: 0.5, + }; + + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: true, + dsc: { sample_rand: '0.0126', sample_rate: '0.5' }, }; const currentSpanStart = timestampInSeconds(); @@ -119,7 +151,7 @@ describe('addPreviousTraceSpanLink', () => { startTimestamp: currentSpanStart, }); - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); + const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan, oldPropagationContext); expect(spanToJSON(currentSpan).links).toEqual([ { @@ -143,6 +175,8 @@ describe('addPreviousTraceSpanLink', () => { expect(updatedPreviousTraceInfo).toEqual({ spanContext: currentSpan.spanContext(), startTimestamp: currentSpanStart, + sampleRand: 0.0126, + sampleRate: 0.5, }); }); @@ -150,13 +184,22 @@ describe('addPreviousTraceSpanLink', () => { const currentSpanStart = timestampInSeconds(); const currentSpan = new SentrySpan({ name: 'test', startTimestamp: currentSpanStart }); - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(undefined, currentSpan); + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: false, + dsc: { sample_rand: '0.0126', sample_rate: '0.5', sampled: 'false' }, + }; + + const updatedPreviousTraceInfo = addPreviousTraceSpanLink(undefined, currentSpan, oldPropagationContext); const spanJson = spanToJSON(currentSpan); expect(spanJson.links).toBeUndefined(); expect(Object.keys(spanJson.data)).not.toContain(PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE); expect(updatedPreviousTraceInfo).toEqual({ + sampleRand: 0.0126, + sampleRate: 0.5, spanContext: currentSpan.spanContext(), startTimestamp: currentSpanStart, }); @@ -178,9 +221,18 @@ describe('addPreviousTraceSpanLink', () => { traceFlags: 1, }, startTimestamp: currentSpanStart - 1, + sampleRand: 0.0126, + sampleRate: 0.5, }; - const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan); + const oldPropagationContext = { + sampleRand: 0.0126, + traceId: '123', + sampled: true, + dsc: { sample_rand: '0.0126', sample_rate: '0.5' }, + }; + + const updatedPreviousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, currentSpan, oldPropagationContext); const spanJson = spanToJSON(currentSpan); expect(spanJson.links).toBeUndefined(); @@ -213,6 +265,8 @@ describe('store and retrieve previous trace data via sessionStorage ', () => { traceFlags: 1, }, startTimestamp: Date.now() / 1000, + sampleRand: 0.0126, + sampleRate: 0.5, }; storePreviousTraceInSessionStorage(previousTraceInfo); @@ -231,6 +285,8 @@ describe('store and retrieve previous trace data via sessionStorage ', () => { traceFlags: 1, }, startTimestamp: Date.now() / 1000, + sampleRand: 0.0126, + sampleRate: 0.5, }; expect(() => storePreviousTraceInSessionStorage(previousTraceInfo)).not.toThrow(); @@ -238,3 +294,24 @@ describe('store and retrieve previous trace data via sessionStorage ', () => { expect(getPreviousTraceFromSessionStorage()).toBeUndefined(); }); }); + +describe('spanContextSampled', () => { + it('returns true if traceFlags is 1', () => { + const spanContext = { + traceId: '123', + spanId: '456', + traceFlags: 1, + }; + + expect(spanContextSampled(spanContext)).toBe(true); + }); + + it.each([0, 2, undefined as unknown as number])('returns false if traceFlags is %s', flags => { + const spanContext = { + traceId: '123', + spanId: '456', + traceFlags: flags, + }; + expect(spanContextSampled(spanContext)).toBe(false); + }); +}); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 46854c7992bd..0dda6c86fd26 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -491,6 +491,7 @@ export abstract class Client { spanAttributes: SpanAttributes; spanName: string; parentSampled?: boolean; + parentSampleRate?: number; parentContext?: SpanContextData; }, samplingDecision: { decision: boolean }, @@ -691,6 +692,7 @@ export abstract class Client { spanAttributes: SpanAttributes; spanName: string; parentSampled?: boolean; + parentSampleRate?: number; parentContext?: SpanContextData; }, samplingDecision: { decision: boolean }, diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index aa25b70f7304..9b90809c0091 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -13,6 +13,14 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source'; */ export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; +/** + * Attribute holding the sample rate of the previous trace. + * This is used to sample consistently across subsequent traces in the browser SDK. + * + * Note: Only defined on root spans, if opted into consistent sampling + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE = 'sentry.previous_trace_sample_rate'; + /** * Use this attribute to represent the operation of a span. */ diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index ce2d9ad7eadd..87e2b0702176 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -2,7 +2,11 @@ import type { Client } from '../client'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; import type { Scope } from '../scope'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '../semanticAttributes'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; @@ -85,8 +89,12 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly): Partial { + console.log('xx applyLocalSampleRateToDsc', dsc, { rootSpanSampleRate }, rootSpanAttributes); if (typeof rootSpanSampleRate === 'number' || typeof rootSpanSampleRate === 'string') { dsc.sample_rate = `${rootSpanSampleRate}`; } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index fb8a89f0f860..a96159692ac3 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -401,7 +401,17 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent const client = getClient(); const options: Partial = client?.getOptions() || {}; - const { name = '', attributes } = spanArguments; + const { name = '' } = spanArguments; + + const mutableSpanSamplingData = { spanAttributes: { ...spanArguments.attributes }, spanName: name, parentSampled }; + + // we don't care about the decision for the moment; this is just a placeholder + client?.emit('beforeSampling', mutableSpanSamplingData, { decision: false }); + + // If hook consumers override the parentSampled flag, we will use that value instead of the actual one + const finalParentSampled = mutableSpanSamplingData.parentSampled ?? parentSampled; + const finalAttributes = mutableSpanSamplingData.spanAttributes; + const currentPropagationContext = scope.getPropagationContext(); const [sampled, sampleRate, localSampleRateWasApplied] = scope.getScopeData().sdkProcessingMetadata[ SUPPRESS_TRACING_KEY @@ -411,8 +421,8 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent options, { name, - parentSampled, - attributes, + parentSampled: finalParentSampled, + attributes: finalAttributes, parentSampleRate: parseSampleRate(currentPropagationContext.dsc?.sample_rate), }, currentPropagationContext.sampleRand, @@ -424,7 +434,7 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate !== undefined && localSampleRateWasApplied ? sampleRate : undefined, - ...spanArguments.attributes, + ...finalAttributes, }, sampled, }); From fdb6cddcafa3cc11f4b6ad11cec6fa1afaa406fc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 11 Apr 2025 17:27:59 +0200 Subject: [PATCH 2/7] add integration tests --- .../consistent-sampling-meta-negative/init.js | 17 ++ .../subject.js | 17 ++ .../template.html | 13 ++ .../consistent-sampling-meta-negative/test.ts | 116 +++++++++++ .../consistent-sampling-meta/init.js | 17 ++ .../consistent-sampling-meta/subject.js | 17 ++ .../consistent-sampling-meta/template.html | 14 ++ .../consistent-sampling-meta/test.ts | 182 ++++++++++++++++++ .../consistent-sampling/init.js | 21 ++ .../consistent-sampling/subject.js | 17 ++ .../consistent-sampling/template.html | 10 + .../consistent-sampling/test.ts | 159 +++++++++++++++ 12 files changed, 600 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js new file mode 100644 index 000000000000..c536f134da0f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + linkPreviousTrace: 'in-memory', + sampleLinkedTracesConsistently: true + }), + ], + tracePropagationTargets: ['someurl.com'], + tracesSampleRate: 1, + debug: true, + sendClientReports: true +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js new file mode 100644 index 000000000000..e200e8eb6382 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js @@ -0,0 +1,17 @@ +const btn1 = document.getElementById('btn1'); + +const btn2 = document.getElementById('btn2'); + +btn1.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + }); +}); + +btn2.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + await fetch('https://someUrl.com'); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html new file mode 100644 index 000000000000..6347fa37fc00 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts new file mode 100644 index 000000000000..d97c27308662 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts @@ -0,0 +1,116 @@ +import { expect } from '@playwright/test'; +import type { ClientReport } from '@sentry/core'; +import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { + envelopeRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipTracingTest, + waitForClientReportRequest, +} from '../../../../../utils/helpers'; + +const metaTagSampleRand = 0.9; +const metaTagSampleRate = 0.2; +const metaTagTraceId = '12345678901234567890123456789012'; + +sentryTest.describe('When `sampleLinkedTracesConsistently` is `true` and page contains tags', () => { + sentryTest( + 'Continues negative sampling decision from meta tag across all traces and downstream propagations', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + let txnsReceived = 0; + // @ts-expect-error - no need to return something valid here + getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'transaction' }, () => { + ++txnsReceived; + return {}; + }); + + const clientReportPromise = waitForClientReportRequest(page); + + await sentryTest.step('Initial pageload', async () => { + await page.goto(url); + expect(txnsReceived).toEqual(0); + }); + + await sentryTest.step('Custom instrumented button click', async () => { + await page.locator('#btn1').click(); + expect(txnsReceived).toEqual(0); + }); + + await sentryTest.step('Navigation', async () => { + await page.goto(`${url}#foo`); + expect(txnsReceived).toEqual(0); + }); + + await sentryTest.step('Make fetch request', async () => { + let sentryTrace = undefined; + let baggage = undefined; + + await page.route('https://someUrl.com', (route, req) => { + baggage = req.headers()['baggage']; + sentryTrace = req.headers()['sentry-trace']; + return route.fulfill({ status: 200, body: 'ok' }); + }); + + await page.locator('#btn2').click(); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: expect.not.stringContaining(metaTagTraceId), + parentSpanId: expect.stringMatching(/^[0-9a-f]{16}$/), + parentSampled: false, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${metaTagSampleRand}`, + 'sentry-sample_rate': `${metaTagSampleRate}`, + 'sentry-sampled': 'false', + 'sentry-trace_id': expect.not.stringContaining(metaTagTraceId), + 'sentry-transaction': 'custom root span 2', + }); + }); + + await sentryTest.step('Client report', async () => { + await page.evaluate(() => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + // Dispatch the visibilitychange event to notify listeners + document.dispatchEvent(new Event('visibilitychange')); + }); + + const clientReport = envelopeRequestParser(await clientReportPromise); + expect(clientReport).toEqual({ + timestamp: expect.any(Number), + discarded_events: [ + { + category: 'transaction', + quantity: 4, + reason: 'sample_rate', + }, + ], + }); + }); + + await sentryTest.step('Wait for transactions to be discarded', async () => { + // give it a little longer just in case a txn is pending to be sent + await page.waitForTimeout(1000); + expect(txnsReceived).toEqual(0); + }); + }, + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js new file mode 100644 index 000000000000..42d1329f51f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + linkPreviousTrace: 'in-memory', + sampleLinkedTracesConsistently: true + }), + ], + tracePropagationTargets: ['someurl.com'], + // only take into account sampling from meta tag; otherwise sample negatively + tracesSampleRate: 0, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js new file mode 100644 index 000000000000..e200e8eb6382 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js @@ -0,0 +1,17 @@ +const btn1 = document.getElementById('btn1'); + +const btn2 = document.getElementById('btn2'); + +btn1.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + }); +}); + +btn2.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + await fetch('https://someUrl.com'); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html new file mode 100644 index 000000000000..c6a798a60c24 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts new file mode 100644 index 000000000000..89134b9de316 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts @@ -0,0 +1,182 @@ +import { expect } from '@playwright/test'; +import { + extractTraceparentData, + parseBaggageHeader, + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, +} from '@sentry/core'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { + eventAndTraceHeaderRequestParser, + shouldSkipTracingTest, + waitForTransactionRequest, +} from '../../../../../utils/helpers'; + +const metaTagSampleRand = 0.051121; +const metaTagSampleRate = 0.2; + +sentryTest.describe('When `sampleLinkedTracesConsistently` is `true` and page contains tags', () => { + sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + await page.goto(url); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + + return pageloadTraceContext; + }); + + const customTraceContext = await sentryTest.step('Custom trace', async () => { + const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + + await page.locator('#btn1').click(); + + const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( + await customTrace1RequestPromise, + ); + + const customTraceContext = customTrace1Event.contexts?.trace; + + expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(customTraceContext?.parent_span_id).toBeUndefined(); + + expect(Number(customTraceTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(customTraceTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + expect(Boolean(customTraceTraceHeader?.sampled)).toBe(true); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + + // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) + expect(customTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe(metaTagSampleRate); + + return customTraceContext; + }); + + await sentryTest.step('Navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest( + page, + evt => evt.contexts?.trace?.op === 'navigation', + ); + + await page.goto(`${url}#foo`); + + const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( + await navigation1RequestPromise, + ); + + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(navigationTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); + + expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + + expect(Number(navigationTraceHeader?.sample_rand)).toEqual(metaTagSampleRand); + expect(Number(navigationTraceHeader?.sample_rate)).toEqual(metaTagSampleRate); + expect(Boolean(navigationTraceHeader?.sampled)).toEqual(true); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + + // but we need to set this attribute to still be able to correctly add the sample rate to the DSC (checked above in trace header) + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( + metaTagSampleRate, + ); + }); + }); + + sentryTest( + 'Propagates continued tag sampling decision to outgoing requests', + async ({ page, getLocalTestUrl }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + await page.goto(url); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(metaTagSampleRand); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(metaTagSampleRate); + + // since the local sample rate was not applied, the sample rate attribute shouldn't be set + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBeUndefined(); + + return pageloadTraceContext; + }); + + await sentryTest.step('Make fetch request', async () => { + let sentryTrace = undefined; + let baggage = undefined; + + await page.route('https://someUrl.com', (route, req) => { + baggage = req.headers()['baggage']; + sentryTrace = req.headers()['sentry-trace']; + return route.fulfill({ status: 200, body: 'ok' }); + }); + + const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + + await page.locator('#btn2').click(); + + const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); + + const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); + const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; + const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + + expect(fetchTraceSampleRand).toEqual(metaTagSampleRand); + + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBeUndefined(); + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]).toBe( + metaTagSampleRate, + ); + + expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: fetchTraceTraceContext?.trace_id, + parentSpanId: httpClientSpan?.span_id, + parentSampled: true, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${metaTagSampleRand}`, + 'sentry-sample_rate': `${metaTagSampleRate}`, + 'sentry-sampled': 'true', + 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-transaction': 'custom root span 2', + }); + }); + }, + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js new file mode 100644 index 000000000000..87222e42e539 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + linkPreviousTrace: 'in-memory', + sampleLinkedTracesConsistently: true + }), + ], + tracePropagationTargets: ['someurl.com'], + tracesSampler: ctx => { + if (ctx.attributes && ctx.attributes['sentry.origin'] === 'auto.pageload.browser') { + return 1; + } + return ctx.inheritOrSampleWith(0); + }, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js new file mode 100644 index 000000000000..e200e8eb6382 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js @@ -0,0 +1,17 @@ +const btn1 = document.getElementById('btn1'); + +const btn2 = document.getElementById('btn2'); + +btn1.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + }); +}); + +btn2.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + await fetch('https://someUrl.com'); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html new file mode 100644 index 000000000000..f27a71d043f9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts new file mode 100644 index 000000000000..5d0606da024b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts @@ -0,0 +1,159 @@ +import { expect } from '@playwright/test'; +import { + extractTraceparentData, + parseBaggageHeader, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, +} from '@sentry/core'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { + eventAndTraceHeaderRequestParser, + shouldSkipTracingTest, + waitForTransactionRequest, +} from '../../../../../utils/helpers'; + +sentryTest.describe('When `sampleLinkedTracesConsistently` is `true`', () => { + sentryTest('Continues sampling decision from initial pageload', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === 'pageload'; + }); + await page.goto(url); + + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + const pageloadTraceContext = res[0].contexts?.trace; + + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + + return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; + }); + + const customTraceContext = await sentryTest.step('Custom trace', async () => { + const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + await page.locator('#btn1').click(); + const [customTrace1Event, customTraceTraceHeader] = eventAndTraceHeaderRequestParser( + await customTrace1RequestPromise, + ); + + const customTraceContext = customTrace1Event.contexts?.trace; + + expect(customTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + // although we "continue the trace" from pageload, this is actually a root span, + // so there must not be a parent span id + expect(customTraceContext?.parent_span_id).toBeUndefined(); + + expect(pageloadSampleRand).toEqual(Number(customTraceTraceHeader?.sample_rand)); + + return customTraceContext; + }); + + await sentryTest.step('Navigation', async () => { + const navigation1RequestPromise = waitForTransactionRequest( + page, + evt => evt.contexts?.trace?.op === 'navigation', + ); + await page.goto(`${url}#foo`); + const [navigationEvent, navigationTraceHeader] = eventAndTraceHeaderRequestParser( + await navigation1RequestPromise, + ); + const navTraceContext = navigationEvent.contexts?.trace; + + expect(navTraceContext?.trace_id).not.toEqual(customTraceContext?.trace_id); + expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(navTraceContext?.links).toEqual([ + { + trace_id: customTraceContext?.trace_id, + span_id: customTraceContext?.span_id, + sampled: true, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', + }, + }, + ]); + expect(navTraceContext?.parent_span_id).toBeUndefined(); + + expect(pageloadSampleRand).toEqual(Number(navigationTraceHeader?.sample_rand)); + }); + }); + + sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const { pageloadTraceContext, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.goto(url); + + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + + const pageloadTraceContext = res[0].contexts?.trace; + + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + + return { pageloadTraceContext: pageloadTraceContext, pageloadSampleRand }; + }); + + await sentryTest.step('Make fetch request', async () => { + let sentryTrace = undefined; + let baggage = undefined; + + await page.route('https://someUrl.com', (route, req) => { + baggage = req.headers()['baggage']; + sentryTrace = req.headers()['sentry-trace']; + return route.fulfill({ status: 200, body: 'ok' }); + }); + + const fetchTracePromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + + await page.locator('#btn2').click(); + + const [fetchTraceEvent, fetchTraceTraceHeader] = eventAndTraceHeaderRequestParser(await fetchTracePromise); + + const fetchTraceSampleRand = Number(fetchTraceTraceHeader?.sample_rand); + const fetchTraceTraceContext = fetchTraceEvent.contexts?.trace; + const httpClientSpan = fetchTraceEvent.spans?.find(span => span.op === 'http.client'); + + expect(fetchTraceSampleRand).toEqual(pageloadSampleRand); + + expect(fetchTraceTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toEqual( + pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE], + ); + expect(fetchTraceTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: fetchTraceTraceContext?.trace_id, + parentSpanId: httpClientSpan?.span_id, + parentSampled: true, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${pageloadSampleRand}`, + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-trace_id': fetchTraceTraceContext?.trace_id, + 'sentry-transaction': 'custom root span 2', + }); + }); + }); +}); From 677cbb97f7b0aec3d48d796d477eea6c6e8d5e72 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 11 Apr 2025 17:35:53 +0200 Subject: [PATCH 3/7] Update packages/core/src/tracing/dynamicSamplingContext.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/core/src/tracing/dynamicSamplingContext.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 87e2b0702176..9380c75dd3be 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -94,7 +94,6 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly): Partial { - console.log('xx applyLocalSampleRateToDsc', dsc, { rootSpanSampleRate }, rootSpanAttributes); if (typeof rootSpanSampleRate === 'number' || typeof rootSpanSampleRate === 'string') { dsc.sample_rate = `${rootSpanSampleRate}`; } From f06a051b2370f28e5b3d05d9fb2cbbfc7d210f64 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Apr 2025 18:14:44 +0200 Subject: [PATCH 4/7] fix meta tag precedence, add more tests, move some files and code around --- .../consistent-sampling/default}/init.js | 2 +- .../consistent-sampling/default}/subject.js | 4 +- .../default}/template.html | 0 .../consistent-sampling/default}/test.ts | 7 +- .../meta-negative}/init.js | 4 +- .../meta-negative}/subject.js | 4 +- .../meta-negative}/template.html | 0 .../meta-negative}/test.ts | 21 +-- .../meta-precedence/init.js | 19 +++ .../meta-precedence/page-1.html | 15 ++ .../meta-precedence/page-2.html | 10 ++ .../meta-precedence/subject.js | 17 +++ .../meta-precedence/template.html | 14 ++ .../meta-precedence/test.ts | 109 ++++++++++++++ .../consistent-sampling/meta}/init.js | 2 +- .../consistent-sampling/meta}/subject.js | 4 +- .../consistent-sampling/meta}/template.html | 0 .../consistent-sampling/meta}/test.ts | 7 +- .../tracesSampler-precedence/init.js | 28 ++++ .../tracesSampler-precedence/subject.js | 17 +++ .../tracesSampler-precedence/template.html | 10 ++ .../tracesSampler-precedence/test.ts | 139 ++++++++++++++++++ .../custom-trace/subject.js | 0 .../custom-trace/template.html | 0 .../custom-trace/test.ts | 0 .../default/test.ts | 0 .../init.js | 0 .../interaction-spans/init.js | 0 .../interaction-spans/template.html | 0 .../interaction-spans/test.ts | 0 .../meta/template.html | 0 .../meta/test.ts | 0 .../negatively-sampled/init.js | 0 .../negatively-sampled/test.ts | 0 .../session-storage/init.js | 0 .../session-storage/test.ts | 0 .../utils/helpers.ts | 14 ++ .../src/tracing/browserTracingIntegration.ts | 86 ++--------- .../{previousTrace.ts => linkedTraces.ts} | 93 +++++++++++- .../tracing/browserTracingIntegration.test.ts | 2 +- ...iousTrace.test.ts => linkedTraces.test.ts} | 136 ++++++++++++++++- 41 files changed, 647 insertions(+), 117 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling => linked-traces/consistent-sampling/default}/init.js (91%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling => linked-traces/consistent-sampling/default}/subject.js (65%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling => linked-traces/consistent-sampling/default}/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling => linked-traces/consistent-sampling/default}/test.ts (97%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta-negative => linked-traces/consistent-sampling/meta-negative}/init.js (82%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta-negative => linked-traces/consistent-sampling/meta-negative}/subject.js (65%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta-negative => linked-traces/consistent-sampling/meta-negative}/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta-negative => linked-traces/consistent-sampling/meta-negative}/test.ts (85%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-1.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-2.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta => linked-traces/consistent-sampling/meta}/init.js (90%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta => linked-traces/consistent-sampling/meta}/subject.js (65%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta => linked-traces/consistent-sampling/meta}/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links/consistent-sampling-meta => linked-traces/consistent-sampling/meta}/test.ts (97%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/custom-trace/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/custom-trace/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/custom-trace/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/default/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/interaction-spans/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/interaction-spans/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/interaction-spans/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/meta/template.html (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/meta/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/negatively-sampled/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/negatively-sampled/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/session-storage/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/{previous-trace-links => linked-traces}/session-storage/test.ts (100%) rename packages/browser/src/tracing/{previousTrace.ts => linkedTraces.ts} (55%) rename packages/browser/test/tracing/{previousTrace.test.ts => linkedTraces.test.ts} (69%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js similarity index 91% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js index 87222e42e539..1415ef740b55 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/init.js @@ -7,7 +7,7 @@ Sentry.init({ integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', - sampleLinkedTracesConsistently: true + consistentTraceSampling: true, }), ], tracePropagationTargets: ['someurl.com'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/subject.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/subject.js index e200e8eb6382..1feeadf34b10 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/subject.js @@ -4,13 +4,13 @@ const btn2 = document.getElementById('btn2'); btn1.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {}); }); }); btn2.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => { await fetch('https://someUrl.com'); }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts similarity index 97% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts index 5d0606da024b..9535822906dc 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts @@ -5,15 +5,14 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, } from '@sentry/core'; - -import { sentryTest } from '../../../../../utils/fixtures'; +import { sentryTest } from '../../../../../../utils/fixtures'; import { eventAndTraceHeaderRequestParser, shouldSkipTracingTest, waitForTransactionRequest, -} from '../../../../../utils/helpers'; +} from '../../../../../../utils/helpers'; -sentryTest.describe('When `sampleLinkedTracesConsistently` is `true`', () => { +sentryTest.describe('When `consistentTraceSampling` is `true`', () => { sentryTest('Continues sampling decision from initial pageload', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js similarity index 82% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js index c536f134da0f..0b26aa6be474 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/init.js @@ -7,11 +7,11 @@ Sentry.init({ integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', - sampleLinkedTracesConsistently: true + consistentTraceSampling: true, }), ], tracePropagationTargets: ['someurl.com'], tracesSampleRate: 1, debug: true, - sendClientReports: true + sendClientReports: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/subject.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/subject.js index e200e8eb6382..1feeadf34b10 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/subject.js @@ -4,13 +4,13 @@ const btn2 = document.getElementById('btn2'); btn1.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {}); }); }); btn2.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => { await fetch('https://someUrl.com'); }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts similarity index 85% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts index d97c27308662..b402c611b8f7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta-negative/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-negative/test.ts @@ -1,20 +1,20 @@ import { expect } from '@playwright/test'; import type { ClientReport } from '@sentry/core'; import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; - -import { sentryTest } from '../../../../../utils/fixtures'; +import { sentryTest } from '../../../../../../utils/fixtures'; import { envelopeRequestParser, getMultipleSentryEnvelopeRequests, + hidePage, shouldSkipTracingTest, waitForClientReportRequest, -} from '../../../../../utils/helpers'; +} from '../../../../../../utils/helpers'; const metaTagSampleRand = 0.9; const metaTagSampleRate = 0.2; const metaTagTraceId = '12345678901234567890123456789012'; -sentryTest.describe('When `sampleLinkedTracesConsistently` is `true` and page contains tags', () => { +sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { sentryTest( 'Continues negative sampling decision from meta tag across all traces and downstream propagations', async ({ getLocalTestUrl, page }) => { @@ -81,18 +81,7 @@ sentryTest.describe('When `sampleLinkedTracesConsistently` is `true` and page co }); await sentryTest.step('Client report', async () => { - await page.evaluate(() => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - // Dispatch the visibilitychange event to notify listeners - document.dispatchEvent(new Event('visibilitychange')); - }); - + await hidePage(page); const clientReport = envelopeRequestParser(await clientReportPromise); expect(clientReport).toEqual({ timestamp: expect.any(Number), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js new file mode 100644 index 000000000000..4c65e3d977de --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + linkPreviousTrace: 'session-storage', + consistentTraceSampling: true, + }), + ], + tracePropagationTargets: ['someurl.com'], + tracesSampler: ({ inheritOrSampleWith }) => { + return inheritOrSampleWith(0); + }, + debug: true, + sendClientReports: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-1.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-1.html new file mode 100644 index 000000000000..9a0719b7e505 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-1.html @@ -0,0 +1,15 @@ + + + + + + + + +

Another Page

+ Go To the next page + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-2.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-2.html new file mode 100644 index 000000000000..27cd47bba7c1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/page-2.html @@ -0,0 +1,10 @@ + + + + + + +

Another Page

+ Go To the next page + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/subject.js new file mode 100644 index 000000000000..376b2102e462 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/subject.js @@ -0,0 +1,17 @@ +const btn1 = document.getElementById('btn1'); + +const btn2 = document.getElementById('btn2'); + +btn1?.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {}); + }); +}); + +btn2?.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => { + await fetch('https://someUrl.com'); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/template.html new file mode 100644 index 000000000000..eab1fecca6c4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/template.html @@ -0,0 +1,14 @@ + + + + + + + + Go To another page + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts new file mode 100644 index 000000000000..fcef826804f2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta-precedence/test.ts @@ -0,0 +1,109 @@ +import { expect } from '@playwright/test'; +import type { ClientReport } from '@sentry/core'; +import { extractTraceparentData, parseBaggageHeader } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + eventAndTraceHeaderRequestParser, + hidePage, + shouldSkipTracingTest, + waitForClientReportRequest, + waitForTransactionRequest, +} from '../../../../../../utils/helpers'; + +const metaTagSampleRand = 0.9; +const metaTagSampleRate = 0.2; +const metaTagTraceIdIndex = '12345678901234567890123456789012'; +const metaTagTraceIdPage1 = 'a2345678901234567890123456789012'; + +sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { + sentryTest( + 'meta tag decision has precedence over sampling decision from previous trace in session storage', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const clientReportPromise = waitForClientReportRequest(page); + + await sentryTest.step('Initial pageload', async () => { + await page.goto(url); + }); + + await sentryTest.step('Make fetch request', async () => { + let sentryTrace = undefined; + let baggage = undefined; + + await page.route('https://someUrl.com', (route, req) => { + baggage = req.headers()['baggage']; + sentryTrace = req.headers()['sentry-trace']; + return route.fulfill({ status: 200, body: 'ok' }); + }); + + await page.locator('#btn2').click(); + + expect(sentryTrace).toBeDefined(); + expect(baggage).toBeDefined(); + + expect(extractTraceparentData(sentryTrace)).toEqual({ + traceId: expect.not.stringContaining(metaTagTraceIdIndex), + parentSpanId: expect.stringMatching(/^[0-9a-f]{16}$/), + parentSampled: false, + }); + + expect(parseBaggageHeader(baggage)).toEqual({ + 'sentry-environment': 'production', + 'sentry-public_key': 'public', + 'sentry-sample_rand': `${metaTagSampleRand}`, + 'sentry-sample_rate': `${metaTagSampleRate}`, + 'sentry-sampled': 'false', + 'sentry-trace_id': expect.not.stringContaining(metaTagTraceIdIndex), + 'sentry-transaction': 'custom root span 2', + }); + }); + + await sentryTest.step('Client report', async () => { + await hidePage(page); + + const clientReport = envelopeRequestParser(await clientReportPromise); + expect(clientReport).toEqual({ + timestamp: expect.any(Number), + discarded_events: [ + { + category: 'transaction', + quantity: 2, + reason: 'sample_rate', + }, + ], + }); + }); + + await sentryTest.step('Navigate to another page with meta tags', async () => { + const page1Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.locator('a').click(); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page1Pageload); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); + expect(pageloadTraceContext?.trace_id).toEqual(metaTagTraceIdPage1); + }); + + await sentryTest.step('Navigate to another page without meta tags', async () => { + const page2Pageload = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + await page.locator('a').click(); + + const [pageloadEvent, pageloadTraceHeader] = eventAndTraceHeaderRequestParser(await page2Pageload); + const pageloadTraceContext = pageloadEvent.contexts?.trace; + + expect(Number(pageloadTraceHeader?.sample_rand)).toBe(0.12); + expect(Number(pageloadTraceHeader?.sample_rate)).toBe(0.2); + expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdPage1); + expect(pageloadTraceContext?.trace_id).not.toEqual(metaTagTraceIdIndex); + }); + }, + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js similarity index 90% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js index 42d1329f51f4..e100eb49469a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/init.js @@ -7,7 +7,7 @@ Sentry.init({ integrations: [ Sentry.browserTracingIntegration({ linkPreviousTrace: 'in-memory', - sampleLinkedTracesConsistently: true + consistentTraceSampling: true, }), ], tracePropagationTargets: ['someurl.com'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/subject.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/subject.js index e200e8eb6382..1feeadf34b10 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/subject.js @@ -4,13 +4,13 @@ const btn2 = document.getElementById('btn2'); btn1.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {}); + Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {}); }); }); btn2.addEventListener('click', () => { Sentry.startNewTrace(() => { - Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, async () => { + Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => { await fetch('https://someUrl.com'); }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts similarity index 97% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts index 89134b9de316..af590353546c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/consistent-sampling-meta/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/meta/test.ts @@ -5,18 +5,17 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; - -import { sentryTest } from '../../../../../utils/fixtures'; +import { sentryTest } from '../../../../../../utils/fixtures'; import { eventAndTraceHeaderRequestParser, shouldSkipTracingTest, waitForTransactionRequest, -} from '../../../../../utils/helpers'; +} from '../../../../../../utils/helpers'; const metaTagSampleRand = 0.051121; const metaTagSampleRate = 0.2; -sentryTest.describe('When `sampleLinkedTracesConsistently` is `true` and page contains tags', () => { +sentryTest.describe('When `consistentTraceSampling` is `true` and page contains tags', () => { sentryTest('Continues sampling decision across all traces from meta tag', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js new file mode 100644 index 000000000000..686bbef5f992 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/init.js @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + linkPreviousTrace: 'in-memory', + consistentTraceSampling: true, + enableInp: false, + }), + ], + tracePropagationTargets: ['someurl.com'], + tracesSampler: ctx => { + if (ctx.attributes && ctx.attributes['sentry.origin'] === 'auto.pageload.browser') { + return 1; + } + if (ctx.name === 'custom root span 1') { + return 0; + } + if (ctx.name === 'custom root span 2') { + return 1; + } + return ctx.inheritOrSampleWith(0); + }, + debug: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/subject.js new file mode 100644 index 000000000000..1feeadf34b10 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/subject.js @@ -0,0 +1,17 @@ +const btn1 = document.getElementById('btn1'); + +const btn2 = document.getElementById('btn2'); + +btn1.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {}); + }); +}); + +btn2.addEventListener('click', () => { + Sentry.startNewTrace(() => { + Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => { + await fetch('https://someUrl.com'); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/template.html new file mode 100644 index 000000000000..f27a71d043f9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/template.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts new file mode 100644 index 000000000000..9e896798be90 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/tracesSampler-precedence/test.ts @@ -0,0 +1,139 @@ +import { expect } from '@playwright/test'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import type { ClientReport } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + eventAndTraceHeaderRequestParser, + hidePage, + shouldSkipTracingTest, + waitForClientReportRequest, + waitForTransactionRequest, +} from '../../../../../../utils/helpers'; + +/** + * This test demonstrates that: + * - explicit sampling decisions in `tracesSampler` has precedence over consistent sampling + * - despite consistentTraceSampling being activated, there are still a lot of cases where the trace chain can break + */ +sentryTest.describe('When `consistentTraceSampling` is `true`', () => { + sentryTest('explicit sampling decisions in `tracesSampler` have precedence', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const { pageloadTraceContext } = await sentryTest.step('Initial pageload', async () => { + const pageloadRequestPromise = waitForTransactionRequest(page, evt => { + return evt.contexts?.trace?.op === 'pageload'; + }); + await page.goto(url); + + const res = eventAndTraceHeaderRequestParser(await pageloadRequestPromise); + const pageloadSampleRand = Number(res[1]?.sample_rand); + const pageloadTraceContext = res[0].contexts?.trace; + + expect(pageloadTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(pageloadSampleRand).toBeGreaterThanOrEqual(0); + + return { pageloadTraceContext: res[0].contexts?.trace, pageloadSampleRand }; + }); + + await sentryTest.step('Custom trace is sampled negatively (explicitly in tracesSampler)', async () => { + const clientReportPromise = waitForClientReportRequest(page); + + await page.locator('#btn1').click(); + + await page.waitForTimeout(500); + await hidePage(page); + + const clientReport = envelopeRequestParser(await clientReportPromise); + + expect(clientReport).toEqual({ + timestamp: expect.any(Number), + discarded_events: [ + { + category: 'transaction', + quantity: 1, + reason: 'sample_rate', + }, + ], + }); + }); + + await sentryTest.step('Subsequent navigation trace is also sampled negatively', async () => { + const clientReportPromise = waitForClientReportRequest(page); + + await page.goto(`${url}#foo`); + + await page.waitForTimeout(500); + + await hidePage(page); + + const clientReport = envelopeRequestParser(await clientReportPromise); + + expect(clientReport).toEqual({ + timestamp: expect.any(Number), + discarded_events: [ + { + category: 'transaction', + quantity: 1, + reason: 'sample_rate', + }, + ], + }); + }); + + const { customTrace2Context } = await sentryTest.step( + 'Custom trace 2 is sampled positively (explicitly in tracesSampler)', + async () => { + const customTrace2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom'); + + await page.locator('#btn2').click(); + + const [customTrace2Event] = eventAndTraceHeaderRequestParser(await customTrace2RequestPromise); + + const customTrace2Context = customTrace2Event.contexts?.trace; + + expect(customTrace2Context?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(customTrace2Context?.trace_id).not.toEqual(pageloadTraceContext?.trace_id); + expect(customTrace2Context?.parent_span_id).toBeUndefined(); + + expect(customTrace2Context?.links).toEqual([ + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: false, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + }, + ]); + + return { customTrace2Context }; + }, + ); + + await sentryTest.step('Navigation trace is sampled positively (inherited from previous trace)', async () => { + const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + + await page.goto(`${url}#bar`); + + const [navigationEvent] = eventAndTraceHeaderRequestParser(await navigationRequestPromise); + + const navigationTraceContext = navigationEvent.contexts?.trace; + + expect(navigationTraceContext?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]).toBe(1); + expect(navigationTraceContext?.trace_id).not.toEqual(customTrace2Context?.trace_id); + expect(navigationTraceContext?.parent_span_id).toBeUndefined(); + + expect(navigationTraceContext?.links).toEqual([ + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: customTrace2Context?.span_id, + trace_id: customTrace2Context?.trace_id, + }, + ]); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/custom-trace/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/default/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/interaction-spans/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/meta/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/meta/template.html rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/meta/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/negatively-sampled/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/session-storage/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/session-storage/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/previous-trace-links/session-storage/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 3f3863bd688d..e2e17e1b750c 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -446,3 +446,17 @@ export async function getFirstSentryEnvelopeRequest( return req; } + +export async function hidePage(page: Page): Promise { + await page.evaluate(() => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + // Dispatch the visibilitychange event to notify listeners + document.dispatchEvent(new Event('visibilitychange')); + }); +} diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 8005d104ebb8..939cbd900708 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -10,14 +10,12 @@ import { getDynamicSamplingContextFromSpan, getIsolationScope, getLocationHref, - getRootSpan, GLOBAL_OBJ, logger, propagationContextFromHeaders, registerSpanErrorInstrumentation, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanIsSampled, spanToJSON, @@ -37,13 +35,7 @@ import { import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import { registerBackgroundTabDetection } from './backgroundtab'; -import type { PreviousTraceInfo } from './previousTrace'; -import { - addPreviousTraceSpanLink, - getPreviousTraceFromSessionStorage, - spanContextSampled, - storePreviousTraceInSessionStorage, -} from './previousTrace'; +import { linkTraces } from './linkedTraces'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing'; @@ -168,8 +160,10 @@ export interface BrowserTracingOptions { * * - `'off'`: The previous trace data will not be stored or linked. * - * Note that your `tracesSampleRate` or `tracesSampler` config significantly influences - * how often traces will be linked. + * You can also use {@link BrowserTracingOptions.consistentTraceSampling} to get + * consistent trace sampling of subsequent traces. Otherwise, by default, your + * `tracesSampleRate` or `tracesSampler` config significantly influences how often + * traces will be linked. * * @default 'in-memory' - see explanation above */ @@ -182,7 +176,7 @@ export interface BrowserTracingOptions { * are also sampled positively. In case the initial trace was sampled negatively, * all subsequent traces are also sampled negatively. * - * This option lets you get consistent, linked traces within a user journey + * This option allows you to get consistent, linked traces within a user journey * while maintaining an overall quota based on your trace sampling settings. * * This option is only effective if {@link BrowserTracingOptions.linkPreviousTrace} @@ -190,7 +184,7 @@ export interface BrowserTracingOptions { * * @default `false` - this is an opt-in feature. */ - sampleLinkedTracesConsistently: boolean; + consistentTraceSampling: boolean; /** * _experiments allows the user to send options to define how this integration works. @@ -233,7 +227,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, linkPreviousTrace: 'in-memory', - sampleLinkedTracesConsistently: false, + consistentTraceSampling: false, _experiments: {}, ...defaultRequestInstrumentationOptions, }; @@ -285,7 +279,7 @@ export const browserTracingIntegration = ((_options: Partial { - if (getRootSpan(span) !== span) { - return; - } - - const scope = getCurrentScope(); - const oldPropagationContext = scope.getPropagationContext(); - inMemoryPreviousTraceInfo = addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span, oldPropagationContext); - - if (useSessionStorage) { - storePreviousTraceInSessionStorage(inMemoryPreviousTraceInfo); - } - }); - - if (sampleLinkedTracesConsistently) { - /* - This is a massive hack I'm really not proud of: - - When users opt into `sampleLinkedTracesConsistently`, we need to make sure that we "propagate" - the previous trace's sample rate and rand to the current trace. This is necessary because otherwise, span - metric extrapolation is off, as we'd be propagating a too high sample rate for the subsequent traces. - - So therefore, we pretend that the previous trace was the parent trace of the newly started trace. To do that, - we mutate the propagation context of the current trace and set the sample rate and sample rand of the previous trace. - Timing-wise, it is fine because it happens before we even sample the root span. - - @see https://github.com/getsentry/sentry-javascript/issues/15754 - */ - client.on('beforeSampling', mutableSamplingContextData => { - if (!inMemoryPreviousTraceInfo) { - return; - } - - const scope = getCurrentScope(); - const currentPropagationContext = scope.getPropagationContext(); - - scope.setPropagationContext({ - ...currentPropagationContext, - dsc: { - ...currentPropagationContext.dsc, - // The fallback to 0 should never happen; this is rather to satisfy the types - sample_rate: String(inMemoryPreviousTraceInfo.sampleRate ?? 0), - sampled: String(spanContextSampled(inMemoryPreviousTraceInfo.spanContext)), - }, - sampleRand: inMemoryPreviousTraceInfo.sampleRand, - }); - - mutableSamplingContextData.parentSampled = spanContextSampled(inMemoryPreviousTraceInfo.spanContext); - mutableSamplingContextData.parentSampleRate = inMemoryPreviousTraceInfo.sampleRate; - - mutableSamplingContextData.spanAttributes = { - ...mutableSamplingContextData.spanAttributes, - // record an attribute that this span was "force-sampled", so that we can later check on this. - [SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]: inMemoryPreviousTraceInfo.sampleRate, - }; - }); - } + linkTraces({ linkPreviousTrace, consistentTraceSampling }, client); } if (WINDOW.location) { diff --git a/packages/browser/src/tracing/previousTrace.ts b/packages/browser/src/tracing/linkedTraces.ts similarity index 55% rename from packages/browser/src/tracing/previousTrace.ts rename to packages/browser/src/tracing/linkedTraces.ts index 443c415d869d..420e8b71be60 100644 --- a/packages/browser/src/tracing/previousTrace.ts +++ b/packages/browser/src/tracing/linkedTraces.ts @@ -1,7 +1,10 @@ -import type { PropagationContext, Span } from '@sentry/core'; +import type { Client, PropagationContext, Span } from '@sentry/core'; import { type SpanContextData, + getCurrentScope, + getRootSpan, logger, + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, spanToJSON, @@ -39,6 +42,90 @@ export const PREVIOUS_TRACE_KEY = 'sentry_previous_trace'; export const PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE = 'sentry.previous_trace'; +/** + * Takes care of linking traces and applying the (consistent) sampling behavoiour based on the passed options + * @param options - options for linking traces and consistent trace sampling (@see BrowserTracingOptions) + * @param client - Sentry client + */ +export function linkTraces( + { + linkPreviousTrace, + consistentTraceSampling, + }: { + linkPreviousTrace: 'session-storage' | 'in-memory'; + consistentTraceSampling: boolean; + }, + client: Client, +): void { + const useSessionStorage = linkPreviousTrace === 'session-storage'; + + let inMemoryPreviousTraceInfo = useSessionStorage ? getPreviousTraceFromSessionStorage() : undefined; + + client.on('spanStart', span => { + if (getRootSpan(span) !== span) { + return; + } + + const oldPropagationContext = getCurrentScope().getPropagationContext(); + inMemoryPreviousTraceInfo = addPreviousTraceSpanLink(inMemoryPreviousTraceInfo, span, oldPropagationContext); + + if (useSessionStorage) { + storePreviousTraceInSessionStorage(inMemoryPreviousTraceInfo); + } + }); + + let isFirstTraceOnPageload = true; + if (consistentTraceSampling) { + /* + When users opt into `consistentTraceSampling`, we need to ensure that we propagate + the previous trace's sample rate and rand to the current trace. This is necessary because otherwise, span + metric extrapolation is inaccurate, as we'd propagate a too high sample rate for the subsequent traces. + + So therefore, we pretend that the previous trace was the parent trace of the newly started trace. To do that, + we mutate the propagation context of the current trace and set the sample rate and sample rand of the previous trace. + Timing-wise, it is fine because it happens before we even sample the root span. + + @see https://github.com/getsentry/sentry-javascript/issues/15754 + */ + client.on('beforeSampling', mutableSamplingContextData => { + if (!inMemoryPreviousTraceInfo) { + return; + } + + const scope = getCurrentScope(); + const currentPropagationContext = scope.getPropagationContext(); + + // We do not want to force-continue the sampling decision if we continue a trace + // that was started on the backend. Most prominently, this will happen in MPAs where + // users hard-navigate between pages. In this case, the sampling decision of a potentially + // started trace on the server takes precedence. + // Why? We want to prioritize inter-trace consistency over intra-trace consistency. + if (isFirstTraceOnPageload && currentPropagationContext.parentSpanId) { + isFirstTraceOnPageload = false; + return; + } + + scope.setPropagationContext({ + ...currentPropagationContext, + dsc: { + ...currentPropagationContext.dsc, + sample_rate: String(inMemoryPreviousTraceInfo.sampleRate), + sampled: String(spanContextSampled(inMemoryPreviousTraceInfo.spanContext)), + }, + sampleRand: inMemoryPreviousTraceInfo.sampleRand, + }); + + mutableSamplingContextData.parentSampled = spanContextSampled(inMemoryPreviousTraceInfo.spanContext); + mutableSamplingContextData.parentSampleRate = inMemoryPreviousTraceInfo.sampleRate; + + mutableSamplingContextData.spanAttributes = { + ...mutableSamplingContextData.spanAttributes, + [SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE]: inMemoryPreviousTraceInfo.sampleRate, + }; + }); + } +} + /** * Adds a previous_trace span link to the passed span if the passed * previousTraceInfo is still valid. @@ -147,6 +234,6 @@ export function getPreviousTraceFromSessionStorage(): PreviousTraceInfo | undefi /** * see {@link import('@sentry/core').spanIsSampled} */ -export const spanContextSampled = (ctx: SpanContextData): boolean => { +export function spanContextSampled(ctx: SpanContextData): boolean { return ctx.traceFlags === 0x1; -}; +} diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 7d6c308a3ca5..728bee5fd1dd 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -28,7 +28,7 @@ import { startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, } from '../../src/tracing/browserTracingIntegration'; -import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/previousTrace'; +import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/linkedTraces'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; const oldTextEncoder = global.window.TextEncoder; diff --git a/packages/browser/test/tracing/previousTrace.test.ts b/packages/browser/test/tracing/linkedTraces.test.ts similarity index 69% rename from packages/browser/test/tracing/previousTrace.test.ts rename to packages/browser/test/tracing/linkedTraces.test.ts index 550c48ebe013..6d5df7db594c 100644 --- a/packages/browser/test/tracing/previousTrace.test.ts +++ b/packages/browser/test/tracing/linkedTraces.test.ts @@ -1,15 +1,145 @@ -import { SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; +import { addChildSpanToSpan, getCurrentScope, SentrySpan, Span, spanToJSON, timestampInSeconds } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { PreviousTraceInfo } from '../../src/tracing/previousTrace'; +import { BrowserClient } from '../../src'; +import type { PreviousTraceInfo } from '../../src/tracing/linkedTraces'; import { addPreviousTraceSpanLink, getPreviousTraceFromSessionStorage, + linkTraces, PREVIOUS_TRACE_KEY, PREVIOUS_TRACE_MAX_DURATION, PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, spanContextSampled, storePreviousTraceInSessionStorage, -} from '../../src/tracing/previousTrace'; +} from '../../src/tracing/linkedTraces'; + +describe('linkTraces', () => { + describe('adds a previous trace span link on span start', () => { + // @ts-expect-error - mock contains only necessary API + const client = new BrowserClient({ transport: () => {}, integrations: [], stackParser: () => [] }); + + let spanStartCb: (span: Span) => void; + + // @ts-expect-error - this is fine for testing + const clientOnSpy = vi.spyOn(client, 'on').mockImplementation((event, cb) => { + // @ts-expect-error - this is fine for testing + if (event === 'spanStart') { + spanStartCb = cb; + } + }); + + it('registers a spanStart handler', () => { + expect(clientOnSpy).toHaveBeenCalledWith('spanStart', expect.any(Function)); + expect(clientOnSpy).toHaveBeenCalledOnce(); + }); + + beforeEach(() => { + linkTraces({ linkPreviousTrace: 'in-memory', consistentTraceSampling: false }, client); + }); + + it("doesn't add a link if the passed span is not the root span", () => { + const rootSpan = new SentrySpan({ + name: 'test', + parentSpanId: undefined, + sampled: true, + spanId: '123', + traceId: '456', + }); + + const childSpan = new SentrySpan({ + name: 'test', + parentSpanId: '123', + spanId: '456', + traceId: '789', + sampled: true, + }); + + addChildSpanToSpan(rootSpan, childSpan); + + spanStartCb(childSpan); + + expect(spanToJSON(childSpan).links).toBeUndefined(); + }); + + it('adds a link from the first trace root span to the second trace root span', () => { + const rootSpanTrace1 = new SentrySpan({ + name: 'test', + parentSpanId: undefined, + sampled: true, + spanId: '123', + traceId: '456', + }); + + spanStartCb(rootSpanTrace1); + + expect(spanToJSON(rootSpanTrace1).links).toBeUndefined(); + + const rootSpanTrace2 = new SentrySpan({ + name: 'test', + parentSpanId: undefined, + sampled: true, + spanId: '789', + traceId: 'def', + }); + + spanStartCb(rootSpanTrace2); + + expect(spanToJSON(rootSpanTrace2).links).toEqual([ + { + attributes: { + 'sentry.link.type': 'previous_trace', + }, + span_id: '123', + trace_id: '456', + sampled: true, + }, + ]); + }); + + it("doesn't add a link to the second root span if it is part of the same trace", () => { + const rootSpanTrace1 = new SentrySpan({ + name: 'test', + parentSpanId: undefined, + sampled: true, + spanId: '123', + traceId: 'def', + }); + + spanStartCb(rootSpanTrace1); + + expect(spanToJSON(rootSpanTrace1).links).toBeUndefined(); + + const rootSpan2Trace = new SentrySpan({ + name: 'test', + parentSpanId: undefined, + sampled: true, + spanId: '789', + traceId: 'def', + }); + + spanStartCb(rootSpan2Trace); + + expect(spanToJSON(rootSpan2Trace).links).toBeUndefined(); + }); + }); + + // only basic tests here, rest is tested in browser-integration-tests + describe('consistentTraceSampling', () => { + // @ts-expect-error - mock contains only necessary API + const client = new BrowserClient({ transport: () => {}, integrations: [], stackParser: () => [] }); + const clientOnSpy = vi.spyOn(client, 'on'); + + beforeEach(() => { + linkTraces({ linkPreviousTrace: 'in-memory', consistentTraceSampling: true }, client); + }); + + it('registers a beforeSampling handler', () => { + expect(clientOnSpy).toHaveBeenCalledWith('spanStart', expect.any(Function)); + expect(clientOnSpy).toHaveBeenCalledWith('beforeSampling', expect.any(Function)); + expect(clientOnSpy).toHaveBeenCalledTimes(2); + }); + }); +}); describe('addPreviousTraceSpanLink', () => { it(`adds a previous_trace span link to startSpanOptions if the previous trace was created within ${PREVIOUS_TRACE_MAX_DURATION}s`, () => { From 45b7e758e5361a71742a11ba64720c92539410bf Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Apr 2025 18:31:18 +0200 Subject: [PATCH 5/7] fix lint and size-limit --- .size-limit.js | 2 +- packages/browser/test/tracing/linkedTraces.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index d66ece2b690d..4332d409870f 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -47,7 +47,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '76 KB', + limit: '77 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags', diff --git a/packages/browser/test/tracing/linkedTraces.test.ts b/packages/browser/test/tracing/linkedTraces.test.ts index 6d5df7db594c..a73b325b8228 100644 --- a/packages/browser/test/tracing/linkedTraces.test.ts +++ b/packages/browser/test/tracing/linkedTraces.test.ts @@ -1,4 +1,5 @@ -import { addChildSpanToSpan, getCurrentScope, SentrySpan, Span, spanToJSON, timestampInSeconds } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { addChildSpanToSpan, SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../../src'; import type { PreviousTraceInfo } from '../../src/tracing/linkedTraces'; From 9aae9b805f22f9d0f15260582f69b698328b5f4f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Apr 2025 18:42:32 +0200 Subject: [PATCH 6/7] finally fix lint?? --- .../browser-integration-tests/utils/helpers.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index e2e17e1b750c..eebc4c668e66 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -5,7 +5,7 @@ import type { Envelope, EnvelopeItem, EnvelopeItemType, - Event, + Event as SentryEvent, EventEnvelope, EventEnvelopeHeaders, SessionContext, @@ -29,7 +29,7 @@ export const envelopeParser = (request: Request | null): unknown[] => { }); }; -export const envelopeRequestParser = (request: Request | null, envelopeIndex = 2): T => { +export const envelopeRequestParser = (request: Request | null, envelopeIndex = 2): T => { return envelopeParser(request)[envelopeIndex] as T; }; @@ -50,7 +50,7 @@ export const properEnvelopeParser = (request: Request | null): EnvelopeItem[] => return items; }; -export type EventAndTraceHeader = [Event, EventEnvelopeHeaders['trace']]; +export type EventAndTraceHeader = [SentryEvent, EventEnvelopeHeaders['trace']]; /** * Returns the first event item and `trace` envelope header from an envelope. @@ -69,7 +69,7 @@ const properFullEnvelopeParser = (request: Request | null): }; function getEventAndTraceHeader(envelope: EventEnvelope): EventAndTraceHeader { - const event = envelope[1][0]?.[1] as Event | undefined; + const event = envelope[1][0]?.[1] as SentryEvent | undefined; const trace = envelope[0]?.trace; if (!event || !trace) { @@ -79,7 +79,7 @@ function getEventAndTraceHeader(envelope: EventEnvelope): EventAndTraceHeader { return [event, trace]; } -export const properEnvelopeRequestParser = (request: Request | null, envelopeIndex = 1): T => { +export const properEnvelopeRequestParser = (request: Request | null, envelopeIndex = 1): T => { return properEnvelopeParser(request)[0]?.[envelopeIndex] as T; }; @@ -182,13 +182,13 @@ export async function runScriptInSandbox( * * @param {Page} page * @param {string} [url] - * @return {*} {Promise>} + * @return {*} {Promise>} */ -export async function getSentryEvents(page: Page, url?: string): Promise> { +export async function getSentryEvents(page: Page, url?: string): Promise> { if (url) { await page.goto(url); } - const eventsHandle = await page.evaluateHandle>('window.events'); + const eventsHandle = await page.evaluateHandle>('window.events'); return eventsHandle.jsonValue(); } @@ -203,7 +203,7 @@ export async function waitForTransactionRequestOnUrl(page: Page, url: string): P return req; } -export function waitForErrorRequest(page: Page, callback?: (event: Event) => boolean): Promise { +export function waitForErrorRequest(page: Page, callback?: (event: SentryEvent) => boolean): Promise { return page.waitForRequest(req => { const postData = req.postData(); if (!postData) { From 02f7e7e0f70c8736e0bcbe70ede853fb94fcb4e3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Apr 2025 18:43:45 +0200 Subject: [PATCH 7/7] size limit once more :( --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 4332d409870f..8a91e0465b6b 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -219,7 +219,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '38.5 KB', + limit: '39 KB', }, // Node SDK (ESM) {