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', + }); + }); + }); +}); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index d08ffccd7831..61a64f472dd9 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -1,6 +1,8 @@ +/* eslint-disable max-lines */ import type { Page, Request } from '@playwright/test'; import { parseEnvelope } from '@sentry/core'; 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 fab45cd1ed4f..a345cb07e702 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -14,6 +14,7 @@ import { GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, TRACING_DEFAULTS, addNonEnumerableProperty, @@ -36,10 +37,10 @@ import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import { registerBackgroundTabDetection } from './backgroundtab'; import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; -import type { PreviousTraceInfo } from './previousTrace'; import { addPreviousTraceSpanLink, getPreviousTraceFromSessionStorage, + spanContextSampled, storePreviousTraceInSessionStorage, } from './previousTrace'; @@ -172,6 +173,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. * @@ -213,6 +231,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, linkPreviousTrace: 'in-memory', + sampleLinkedTracesConsistently: false, _experiments: {}, ...defaultRequestInstrumentationOptions, }; @@ -253,6 +272,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 91e52b519dad..40b0c09702b2 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 { logger, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, spanToJSON, type SpanContextData } from '@sentry/core'; +import type { PropagationContext, Span } from '@sentry/core'; +import { + logger, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, + spanToJSON, + type SpanContextData, +} from '@sentry/core'; import { WINDOW } from '../exports'; import { DEBUG_BUILD } from '../debug-build'; @@ -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 e3e3e3cc597e..9e4d563d78da 100644 --- a/packages/browser/test/tracing/previousTrace.test.ts +++ b/packages/browser/test/tracing/previousTrace.test.ts @@ -6,6 +6,7 @@ import { PREVIOUS_TRACE_KEY, PREVIOUS_TRACE_MAX_DURATION, PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE, + spanContextSampled, } from '../../src/tracing/previousTrace'; import { SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import { 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 3599448abf4c..35a9be1929c1 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -509,6 +509,7 @@ export abstract class Client { spanAttributes: SpanAttributes; spanName: string; parentSampled?: boolean; + parentSampleRate?: number; parentContext?: SpanContextData; }, samplingDecision: { decision: boolean }, @@ -709,6 +710,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 12cf4ca11ca6..1412ebdfc41f 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, Span } from '../types-hoist'; import { baggageHeaderToDynamicSamplingContext, @@ -84,7 +88,10 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly): Partial { 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 00e004a80131..3b70f07b4f1a 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -407,7 +407,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 @@ -417,8 +427,8 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent options, { name, - parentSampled, - attributes, + parentSampled: finalParentSampled, + attributes: finalAttributes, parentSampleRate: parseSampleRate(currentPropagationContext.dsc?.sample_rate), }, currentPropagationContext.sampleRand, @@ -430,7 +440,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, });