From f35d3f2da7a74631032c6fa4747bba31de90a250 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 2 Sep 2025 18:00:44 +0200 Subject: [PATCH 1/7] feat(browser): Add suppot for `propagateTraceparent` SDK option --- .../defaultTargetsNoMatch/init.js | 1 + .../defaultTargetsNoMatch/test.ts | 3 +- .../fetch-propagateTraceparent/init.js | 11 +++ .../fetch-propagateTraceparent/subject.js | 5 ++ .../fetch-propagateTraceparent/test.ts | 60 ++++++++++++++++ .../init.js | 11 +++ .../subject.js | 1 + .../test.ts | 32 +++++++++ .../init.js | 11 +++ .../subject.js | 1 + .../test.ts | 32 +++++++++ .../init.js | 11 +++ .../subject.js | 3 + .../test.ts | 26 +++++++ .../suites/tracing/request/fetch/test.ts | 6 +- .../request/xhr-propagateTraceparent/init.js | 11 +++ .../xhr-propagateTraceparent/subject.js | 12 ++++ .../request/xhr-propagateTraceparent/test.ts | 58 ++++++++++++++++ .../init.js | 11 +++ .../subject.js | 3 + .../test.ts | 25 +++++++ .../init.js | 10 +++ .../subject.js | 3 + .../test.ts | 32 +++++++++ .../init.js | 11 +++ .../subject.js | 9 +++ .../test.ts | 28 ++++++++ packages/browser/src/client.ts | 29 +++++--- packages/browser/src/tracing/request.ts | 42 ++++++++--- packages/cloudflare/src/integrations/fetch.ts | 4 +- packages/core/src/fetch.ts | 69 +++++++++++++++++-- packages/core/src/index.ts | 5 +- packages/core/src/integration.ts | 6 +- packages/core/src/tracing/sampling.ts | 4 +- packages/core/src/types-hoist/options.ts | 8 +-- packages/core/src/types-hoist/tracing.ts | 1 + packages/core/src/utils/hasSpansEnabled.ts | 4 +- packages/core/src/utils/sdkMetadata.ts | 4 +- packages/core/src/utils/spanUtils.ts | 2 +- packages/core/src/utils/traceData.ts | 36 ++++++++-- packages/core/src/utils/tracing.ts | 3 + .../core/test/lib/utils/traceData.test.ts | 54 +++++++++++++++ packages/core/test/lib/utils/tracing.test.ts | 7 +- .../src/integrations/wintercg-fetch.ts | 10 +-- 44 files changed, 664 insertions(+), 51 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js index 83076460599f..d7b0a648a2de 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/init.js @@ -6,4 +6,5 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, + propagateTraceparent: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts index 9851d51b99d6..ac6931fab85a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts @@ -3,7 +3,7 @@ import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest( - 'should not attach `sentry-trace` and `baggage` header to cross-origin requests when no tracePropagationTargets are defined', + "doesn't attach `sentry-trace` and `baggage` or `traceparent` (if `propagateTraceparent` is true) header to cross-origin requests when no tracePropagationTargets are defined", async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); @@ -25,6 +25,7 @@ sentryTest( expect(requestHeaders).not.toMatchObject({ 'sentry-trace': expect.any(String), baggage: expect.any(String), + traceparent: expect.any(String), }); } }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js new file mode 100644 index 000000000000..cb785bbf7fda --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/subject.js new file mode 100644 index 000000000000..482a738009c2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/subject.js @@ -0,0 +1,5 @@ +fetch('http://sentry-test-site.example/0').then( + fetch('http://sentry-test-site.example/1', { headers: { 'X-Test-Header': 'existing-header' } }).then( + fetch('http://sentry-test-site.example/2'), + ), +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/test.ts new file mode 100644 index 000000000000..d0aedc65bac8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/test.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to fetch requests if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://sentry-test-site.example/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + const traceparentData1 = extractTraceparentData(requestHeaders1['sentry-trace']); + expect(traceparentData1).toMatchObject({ + traceId: expect.stringMatching(/^([a-f0-9]{32})$/), + parentSpanId: expect.stringMatching(/^([a-f0-9]{16})$/), + parentSampled: true, + }); + + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': `${traceparentData1?.traceId}-${traceparentData1?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData1?.traceId}`), + traceparent: `00-${traceparentData1?.traceId}-${traceparentData1?.parentSpanId}-01`, + }); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + const traceparentData2 = extractTraceparentData(requestHeaders2['sentry-trace']); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': `${traceparentData2?.traceId}-${traceparentData2?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData2?.traceId}`), + traceparent: `00-${traceparentData2?.traceId}-${traceparentData2?.parentSpanId}-01`, + 'x-test-header': 'existing-header', + }); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + const traceparentData3 = extractTraceparentData(requestHeaders3['sentry-trace']); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': `${traceparentData3?.traceId}-${traceparentData3?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData3?.traceId}`), + traceparent: `00-${traceparentData3?.traceId}-${traceparentData3?.parentSpanId}-01`, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js new file mode 100644 index 000000000000..fa09e056425e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 0, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/subject.js new file mode 100644 index 000000000000..f5e7312a1961 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/subject.js @@ -0,0 +1 @@ +fetch('http://sentry-test-site.example/0'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/test.ts new file mode 100644 index 000000000000..26815021f0e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to unsampled fetch requests if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, request] = await Promise.all([page.goto(url), page.waitForRequest('http://sentry-test-site.example/0')]); + + const requestHeaders = request.headers(); + + const traceparentData = extractTraceparentData(requestHeaders['sentry-trace']); + expect(traceparentData).toMatchObject({ + traceId: expect.stringMatching(/^([a-f0-9]{32})$/), + parentSpanId: expect.stringMatching(/^([a-f0-9]{16})$/), + parentSampled: false, + }); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': `${traceparentData?.traceId}-${traceparentData?.parentSpanId}-0`, + traceparent: `00-${traceparentData?.traceId}-${traceparentData?.parentSpanId}-00`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData?.traceId}`), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js new file mode 100644 index 000000000000..8d3c1948d89d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + // no tracesSampleRate defined means TWP mode + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/subject.js new file mode 100644 index 000000000000..e9a1ca98b5b0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/subject.js @@ -0,0 +1 @@ +fetch('http://sentry-test-site.example/0').then(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/test.ts new file mode 100644 index 000000000000..22cc7ba98eb8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to tracing without performance (TWP) fetch requests, if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, request] = await Promise.all([page.goto(url), page.waitForRequest('http://sentry-test-site.example/0')]); + + const requestHeaders = request.headers(); + + const traceparentData = extractTraceparentData(requestHeaders['sentry-trace']); + expect(traceparentData).toMatchObject({ + traceId: expect.stringMatching(/^([a-f0-9]{32})$/), + parentSpanId: expect.stringMatching(/^([a-f0-9]{16})$/), + parentSampled: undefined, + }); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': `${traceparentData?.traceId}-${traceparentData?.parentSpanId}`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData?.traceId}`), + traceparent: `00-${traceparentData?.traceId}-${traceparentData?.parentSpanId}-00`, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js new file mode 100644 index 000000000000..cb785bbf7fda --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/subject.js new file mode 100644 index 000000000000..8f5a109f0a6d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/subject.js @@ -0,0 +1,3 @@ +fetch('http://sentry-test-site.example/api/test/', { + headers: { 'sentry-trace': 'abc-123-1', baggage: 'sentry-trace_id=abc', traceparent: '00-abc-123-01' }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/test.ts new file mode 100644 index 000000000000..f1b163fc0778 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/test.ts @@ -0,0 +1,26 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + "instrumentation doesn't override manually added traceparent header, if `propagateTraceparent` is true", + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const requestPromise = page.waitForRequest('http://sentry-test-site.example/api/test/'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const request = await requestPromise; + + const headers = await request.allHeaders(); + + expect(headers['sentry-trace']).toBe('abc-123-1'); + expect(headers.baggage).toBe('sentry-trace_id=abc'); + expect(headers.traceparent).toBe('00-abc-123-01'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index 9e2a2dc9bd8b..4253f7b6e109 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -41,7 +41,7 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, p ); }); -sentryTest('should attach `sentry-trace` header to fetch requests', async ({ getLocalTestUrl, page }) => { +sentryTest('attaches `sentry-trace` and `baggage` headers to fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -65,6 +65,8 @@ sentryTest('should attach `sentry-trace` header to fetch requests', async ({ get 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/), baggage: expect.any(String), }); + // traceparent must only be attached if propagateTraceparent is `true` + expect(requestHeaders1).not.toHaveProperty('traceparent'); const request2 = requests[1]; const requestHeaders2 = request2.headers(); @@ -73,6 +75,7 @@ sentryTest('should attach `sentry-trace` header to fetch requests', async ({ get baggage: expect.any(String), 'x-test-header': 'existing-header', }); + expect(requestHeaders1).not.toHaveProperty('traceparent'); const request3 = requests[2]; const requestHeaders3 = request3.headers(); @@ -80,4 +83,5 @@ sentryTest('should attach `sentry-trace` header to fetch requests', async ({ get 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/), baggage: expect.any(String), }); + expect(requestHeaders1).not.toHaveProperty('traceparent'); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js new file mode 100644 index 000000000000..cb785bbf7fda --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/subject.js new file mode 100644 index 000000000000..9c584bf743cb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/subject.js @@ -0,0 +1,12 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://sentry-test-site.example/0'); +xhr_1.send(); + +const xhr_2 = new XMLHttpRequest(); +xhr_2.open('GET', 'http://sentry-test-site.example/1'); +xhr_2.setRequestHeader('X-Test-Header', 'existing-header'); +xhr_2.send(); + +const xhr_3 = new XMLHttpRequest(); +xhr_3.open('GET', 'http://sentry-test-site.example/2'); +xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/test.ts new file mode 100644 index 000000000000..1289dfb6cdcc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/test.ts @@ -0,0 +1,58 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to XHR requests if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://sentry-test-site.example/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + const traceparentData1 = extractTraceparentData(requestHeaders1['sentry-trace']); + expect(traceparentData1).toMatchObject({ + traceId: expect.stringMatching(/^([a-f0-9]{32})$/), + parentSpanId: expect.stringMatching(/^([a-f0-9]{16})$/), + parentSampled: true, + }); + + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': `${traceparentData1?.traceId}-${traceparentData1?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData1?.traceId}`), + traceparent: `00-${traceparentData1?.traceId}-${traceparentData1?.parentSpanId}-01`, + }); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + const traceparentData2 = extractTraceparentData(requestHeaders2['sentry-trace']); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': `${traceparentData2?.traceId}-${traceparentData2?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData2?.traceId}`), + traceparent: `00-${traceparentData2?.traceId}-${traceparentData2?.parentSpanId}-01`, + 'x-test-header': 'existing-header', + }); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + const traceparentData3 = extractTraceparentData(requestHeaders3['sentry-trace']); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': `${traceparentData3?.traceId}-${traceparentData3?.parentSpanId}-1`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData3?.traceId}`), + traceparent: `00-${traceparentData3?.traceId}-${traceparentData3?.parentSpanId}-01`, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js new file mode 100644 index 000000000000..fa09e056425e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 0, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/subject.js new file mode 100644 index 000000000000..1c0ec68582e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/subject.js @@ -0,0 +1,3 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://sentry-test-site.example/0'); +xhr_1.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/test.ts new file mode 100644 index 000000000000..abcb25776c83 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/test.ts @@ -0,0 +1,25 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to unsampled xhr requests, if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, request] = await Promise.all([page.goto(url), page.waitForRequest('http://sentry-test-site.example/0')]); + + const requestHeaders1 = request.headers(); + const traceparentData = extractTraceparentData(requestHeaders1['sentry-trace']); + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': `${traceparentData?.traceId}-${traceparentData?.parentSpanId}-0`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData?.traceId}`), + traceparent: `00-${traceparentData?.traceId}-${traceparentData?.parentSpanId}-00`, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js new file mode 100644 index 000000000000..db749bf038e0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/subject.js new file mode 100644 index 000000000000..1c0ec68582e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/subject.js @@ -0,0 +1,3 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://sentry-test-site.example/0'); +xhr_1.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/test.ts new file mode 100644 index 000000000000..de1afa51f962 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/test.ts @@ -0,0 +1,32 @@ +import { expect } from '@playwright/test'; +import { extractTraceparentData } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches traceparent header to tracing without performance (TWP) xhr requests, if `propagateTraceparent` is true', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, request] = await Promise.all([page.goto(url), page.waitForRequest('http://sentry-test-site.example/0')]); + + const requestHeaders = request.headers(); + const traceparentData = extractTraceparentData(requestHeaders['sentry-trace']); + + expect(traceparentData).toEqual({ + traceId: expect.stringMatching(/^([a-f0-9]{32})$/), + parentSpanId: expect.stringMatching(/^([a-f0-9]{16})$/), + parentSampled: undefined, + }); + + expect(requestHeaders).toMatchObject({ + 'sentry-trace': `${traceparentData?.traceId}-${traceparentData?.parentSpanId}`, + baggage: expect.stringContaining(`sentry-trace_id=${traceparentData?.traceId}`), + traceparent: `00-${traceparentData?.traceId}-${traceparentData?.parentSpanId}-00`, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js new file mode 100644 index 000000000000..cb785bbf7fda --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + propagateTraceparent: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/subject.js new file mode 100644 index 000000000000..45b43bdfafa9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/subject.js @@ -0,0 +1,9 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://sentry-test-site.example/1'); +xhr.setRequestHeader('X-Test-Header', 'existing-header'); +xhr.setRequestHeader('sentry-trace', '123-abc-1'); +xhr.setRequestHeader('baggage', ' sentry-release=1.1.1, sentry-trace_id=123'); +xhr.setRequestHeader('traceparent', '00-123-abc-01'); + +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/test.ts new file mode 100644 index 000000000000..a9ce2f6de060 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + "instrumentation doesn't override manually added traceparent header, if `propagateTraceparent` is true", + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requestPromise = page.waitForRequest('http://sentry-test-site.example/1'); + + await page.goto(url); + + const request = await requestPromise; + + const requestHeaders = request.headers(); + expect(requestHeaders).toMatchObject({ + 'sentry-trace': '123-abc-1', + baggage: 'sentry-release=1.1.1, sentry-trace_id=123', + 'x-test-header': 'existing-header', + traceparent: '00-123-abc-01', + }); + }, +); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 65561c29e9de..b4e4f24d3b90 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -4,7 +4,7 @@ import type { ClientOptions, Event, EventHint, - Options, + Options as CoreOptions, ParameterizedString, Scope, SeverityLevel, @@ -31,13 +31,7 @@ type BrowserSpecificOptions = BrowserClientReplayOptions & BrowserClientProfilingOptions & { /** If configured, this URL will be used as base URL for lazy loading integration. */ cdnBaseUrl?: string; - }; -/** - * Configuration options for the Sentry Browser SDK. - * @see @sentry/core Options for more information. - */ -export type BrowserOptions = Options & - BrowserSpecificOptions & { + /** * Important: Only set this option if you know what you are doing! * @@ -56,7 +50,26 @@ export type BrowserOptions = Options & * @default false */ skipBrowserExtensionCheck?: boolean; + + /** + * If set to `true`, the SDK propagates the W3C `traceparent` header to any outgoing requests, + * in addition to the `sentry-trace` and `baggage` headers. Use the {@link CoreOptions.tracePropagationTargets} + * option to control to which outgoing requests the header will be attached. + * + * **Important:** If you set this option to `true`, make sure that you configured your servers' + * CORS settings to allow the `traceparent` header. Otherwise, requests might get blocked. + * + * @see https://www.w3.org/TR/trace-context/ + * + * @default false + */ + propagateTraceparent?: boolean; }; +/** + * Configuration options for the Sentry Browser SDK. + * @see @sentry/core Options for more information. + */ +export type BrowserOptions = CoreOptions & BrowserSpecificOptions; /** * Configuration options for the Sentry Browser SDK Client class diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 0756da3ccb2a..7be890c31313 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -24,6 +24,7 @@ import { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, } from '@sentry-internal/browser-utils'; +import type { BrowserClient } from '../client'; import { WINDOW } from '../helpers'; import { resourceTimingToSpanAttributes } from './resource-timing'; @@ -136,6 +137,8 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial = {}; + const propagateTraceparent = (client as BrowserClient).getOptions().propagateTraceparent; + if (traceFetch) { // Keeping track of http requests, whose body payloads resolved later than the initial resolved request // e.g. streaming using server sent events (SSE) @@ -166,7 +169,9 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { - const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); + const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans, { + propagateTraceparent, + }); if (handlerData.response && handlerData.fetchData.__span) { responseToSpanId.set(handlerData.response, handlerData.fetchData.__span); @@ -194,7 +199,14 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { - const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); + const createdSpan = xhrCallback( + handlerData, + shouldCreateSpan, + shouldAttachHeadersWithTargets, + spans, + propagateTraceparent, + ); + if (createdSpan) { if (enableHTTPTimings) { addHTTPTimings(createdSpan); @@ -303,6 +315,7 @@ export function xhrCallback( shouldCreateSpan: (url: string) => boolean, shouldAttachHeaders: (url: string) => boolean, spans: Record, + propagateTraceparent?: boolean, ): Span | undefined { const xhr = handlerData.xhr; const sentryXhrData = xhr?.[SENTRY_XHR_DATA_KEY]; @@ -366,6 +379,7 @@ export function xhrCallback( // we do not want to use the span as base for the trace headers, // which means that the headers will be generated from the scope and the sampling decision is deferred hasSpansEnabled() && hasParent ? span : undefined, + propagateTraceparent, ); } @@ -377,11 +391,15 @@ export function xhrCallback( return span; } -function addTracingHeadersToXhrRequest(xhr: SentryWrappedXMLHttpRequest, span?: Span): void { - const { 'sentry-trace': sentryTrace, baggage } = getTraceData({ span }); +function addTracingHeadersToXhrRequest( + xhr: SentryWrappedXMLHttpRequest, + span?: Span, + propagateTraceparent?: boolean, +): void { + const { 'sentry-trace': sentryTrace, baggage, traceparent } = getTraceData({ span, propagateTraceparent }); if (sentryTrace) { - setHeaderOnXhr(xhr, sentryTrace, baggage); + setHeaderOnXhr(xhr, sentryTrace, baggage, traceparent); } } @@ -389,17 +407,22 @@ function setHeaderOnXhr( xhr: SentryWrappedXMLHttpRequest, sentryTraceHeader: string, sentryBaggageHeader: string | undefined, + traceparentHeader: string | undefined, ): void { const originalHeaders = xhr.__sentry_xhr_v3__?.request_headers; - if (originalHeaders?.['sentry-trace']) { + if (originalHeaders?.['sentry-trace'] || !xhr.setRequestHeader) { // bail if a sentry-trace header is already set return; } try { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - xhr.setRequestHeader!('sentry-trace', sentryTraceHeader); + xhr.setRequestHeader('sentry-trace', sentryTraceHeader); + + if (traceparentHeader && !originalHeaders?.['traceparent']) { + xhr.setRequestHeader('traceparent', traceparentHeader); + } + if (sentryBaggageHeader) { // only add our headers if // - no pre-existing baggage header exists @@ -409,8 +432,7 @@ function setHeaderOnXhr( // From MDN: "If this method is called several times with the same header, the values are merged into one single request header." // We can therefore simply set a baggage header without checking what was there before // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - xhr.setRequestHeader!('baggage', sentryBaggageHeader); + xhr.setRequestHeader('baggage', sentryBaggageHeader); } } } catch { diff --git a/packages/cloudflare/src/integrations/fetch.ts b/packages/cloudflare/src/integrations/fetch.ts index 7f15a7fc1759..66c9f559f29c 100644 --- a/packages/cloudflare/src/integrations/fetch.ts +++ b/packages/cloudflare/src/integrations/fetch.ts @@ -98,7 +98,9 @@ const _fetchIntegration = ((options: Partial = {}) => { return; } - instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, 'auto.http.fetch'); + instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, { + spanOrigin: 'auto.http.fetch', + }); if (breadcrumbs) { createBreadcrumb(handlerData); diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 502e24711994..501c29b4ea10 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -21,6 +21,39 @@ type PolymorphicRequestHeaders = get: (key: string) => string | null | undefined; }; +interface InstrumentFetchRequestOptions { + spanOrigin?: SpanOrigin; + propagateTraceparent?: boolean; +} + +/** + * Create and track fetch request spans for usage in combination with `addFetchInstrumentationHandler`. + * + * @deprecated pass an options object instead of the spanOrigin parameter + * + * @returns Span if a span was created, otherwise void. + */ +export function instrumentFetchRequest( + handlerData: HandlerDataFetch, + shouldCreateSpan: (url: string) => boolean, + shouldAttachHeaders: (url: string) => boolean, + spans: Record, + spanOrigin: SpanOrigin, +): Span | undefined; +/** + * Create and track fetch request spans for usage in combination with `addFetchInstrumentationHandler`. + * + * @returns Span if a span was created, otherwise void. + */ +export function instrumentFetchRequest( + handlerData: HandlerDataFetch, + shouldCreateSpan: (url: string) => boolean, + shouldAttachHeaders: (url: string) => boolean, + spans: Record, + // eslint-disable-next-line @typescript-eslint/unified-signatures -- needed because the other overload is deprecated + instrumentFetchRequestOptions: InstrumentFetchRequestOptions, +): Span | undefined; + /** * Create and track fetch request spans for usage in combination with `addFetchInstrumentationHandler`. * @@ -31,7 +64,7 @@ export function instrumentFetchRequest( shouldCreateSpan: (url: string) => boolean, shouldAttachHeaders: (url: string) => boolean, spans: Record, - spanOrigin: SpanOrigin = 'auto.http.browser', + spanOriginOrOptions?: SpanOrigin | InstrumentFetchRequestOptions, ): Span | undefined { if (!handlerData.fetchData) { return undefined; @@ -55,6 +88,12 @@ export function instrumentFetchRequest( return undefined; } + // Backwards-compatible with the old signature. Needed to introduce the combined optional parameter + // to avoid API breakage for anyone calling this function with the optional spanOrigin parameter + // TODO (v11): remove this backwards-compatible code and only accept the options parameter + const { spanOrigin = 'auto.http.browser', propagateTraceparent = false } = + typeof spanOriginOrOptions === 'object' ? spanOriginOrOptions : { spanOrigin: spanOriginOrOptions }; + const hasParent = !!getActiveSpan(); const span = @@ -77,6 +116,7 @@ export function instrumentFetchRequest( // we do not want to use the span as base for the trace headers, // which means that the headers will be generated from the scope and the sampling decision is deferred hasSpansEnabled() && hasParent ? span : undefined, + propagateTraceparent, ); if (headers) { // Ensure this is actually set, if no options have been passed previously @@ -121,10 +161,12 @@ export function _addTracingHeadersToFetchRequest( | PolymorphicRequestHeaders; }, span?: Span, + propagateTraceparent?: boolean, ): PolymorphicRequestHeaders | undefined { - const traceHeaders = getTraceData({ span }); + const traceHeaders = getTraceData({ span, propagateTraceparent }); const sentryTrace = traceHeaders['sentry-trace']; const baggage = traceHeaders.baggage; + const traceparent = traceHeaders.traceparent; // Nothing to do, when we return undefined here, the original headers will be used if (!sentryTrace) { @@ -143,6 +185,10 @@ export function _addTracingHeadersToFetchRequest( newHeaders.set('sentry-trace', sentryTrace); } + if (propagateTraceparent && traceparent && !newHeaders.get('traceparent')) { + newHeaders.set('traceparent', traceparent); + } + if (baggage) { const prevBaggageHeader = newHeaders.get('baggage'); @@ -161,6 +207,10 @@ export function _addTracingHeadersToFetchRequest( newHeaders.push(['sentry-trace', sentryTrace]); } + if (propagateTraceparent && traceparent && !originalHeaders.find(header => header[0] === 'traceparent')) { + newHeaders.push(['traceparent', traceparent]); + } + const prevBaggageHeaderWithSentryValues = originalHeaders.find( header => header[0] === 'baggage' && baggageHeaderHasSentryBaggageValues(header[1]), ); @@ -174,8 +224,9 @@ export function _addTracingHeadersToFetchRequest( return newHeaders as PolymorphicRequestHeaders; } else { const existingSentryTraceHeader = 'sentry-trace' in originalHeaders ? originalHeaders['sentry-trace'] : undefined; - + const existingTraceparentHeader = 'traceparent' in originalHeaders ? originalHeaders.traceparent : undefined; const existingBaggageHeader = 'baggage' in originalHeaders ? originalHeaders.baggage : undefined; + const newBaggageHeaders: string[] = existingBaggageHeader ? Array.isArray(existingBaggageHeader) ? [...existingBaggageHeader] @@ -192,11 +243,21 @@ export function _addTracingHeadersToFetchRequest( newBaggageHeaders.push(baggage); } - return { + const newHeaders: { + 'sentry-trace': string; + baggage: string | undefined; + traceparent?: string; + } = { ...(originalHeaders as Exclude), 'sentry-trace': (existingSentryTraceHeader as string | undefined) ?? sentryTrace, baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined, }; + + if (propagateTraceparent && traceparent && !existingTraceparentHeader) { + newHeaders.traceparent = traceparent; + } + + return newHeaders; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6385a75687f7..4447eea4dae0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -113,6 +113,9 @@ export { consoleIntegration } from './integrations/console'; export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { profiler } from './profiling'; +// eslint thinks the entire function is deprecated (while only one overload is actually deprecated) +// Therefore: +// eslint-disable-next-line deprecation/deprecation export { instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; export { wrapMcpServerWithSentry } from './integrations/mcp-server'; @@ -342,7 +345,7 @@ export type { Extra, Extras } from './types-hoist/extra'; export type { Integration, IntegrationFn } from './types-hoist/integration'; export type { Mechanism } from './types-hoist/mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types-hoist/misc'; -export type { ClientOptions, Options } from './types-hoist/options'; +export type { ClientOptions, CoreOptions as Options } from './types-hoist/options'; export type { Package } from './types-hoist/package'; export type { PolymorphicEvent, PolymorphicRequest } from './types-hoist/polymorphics'; export type { diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 0cc9fe2630fe..5cba3ff3dfb8 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -3,7 +3,7 @@ import { getClient } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { Integration, IntegrationFn } from './types-hoist/integration'; -import type { Options } from './types-hoist/options'; +import type { CoreOptions } from './types-hoist/options'; import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; @@ -42,7 +42,9 @@ function filterDuplicates(integrations: Integration[]): Integration[] { } /** Gets integrations to install */ -export function getIntegrationsToSetup(options: Pick): Integration[] { +export function getIntegrationsToSetup( + options: Pick, +): Integration[] { const defaultIntegrations = options.defaultIntegrations || []; const userIntegrations = options.integrations; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 27b32970d74c..54cfdab59766 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -1,5 +1,5 @@ import { DEBUG_BUILD } from '../debug-build'; -import type { Options } from '../types-hoist/options'; +import type { CoreOptions } from '../types-hoist/options'; import type { SamplingContext } from '../types-hoist/samplingcontext'; import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; @@ -12,7 +12,7 @@ import { parseSampleRate } from '../utils/parseSampleRate'; * sent to Sentry. */ export function sampleSpan( - options: Pick, + options: Pick, samplingContext: SamplingContext, sampleRand: number, ): [sampled: boolean, sampleRate?: number, localSampleRateWasApplied?: boolean] { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 142313b76c25..92603bb0242d 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -286,7 +286,7 @@ export interface ClientOptions +export interface CoreOptions extends Omit>, 'integrations' | 'transport' | 'stackParser'> { /** * If this is set to false, default integrations will not be added, otherwise this will internally be set to the diff --git a/packages/core/src/types-hoist/tracing.ts b/packages/core/src/types-hoist/tracing.ts index e1dcfef96c6a..60e59cdafa43 100644 --- a/packages/core/src/types-hoist/tracing.ts +++ b/packages/core/src/types-hoist/tracing.ts @@ -59,4 +59,5 @@ export interface PropagationContext { export interface SerializedTraceData { 'sentry-trace'?: string; baggage?: string; + traceparent?: string; } diff --git a/packages/core/src/utils/hasSpansEnabled.ts b/packages/core/src/utils/hasSpansEnabled.ts index 26a71eb7ca0b..cfa447fd86bd 100644 --- a/packages/core/src/utils/hasSpansEnabled.ts +++ b/packages/core/src/utils/hasSpansEnabled.ts @@ -1,5 +1,5 @@ import { getClient } from '../currentScopes'; -import type { Options } from '../types-hoist/options'; +import type { CoreOptions } from '../types-hoist/options'; // Treeshakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean | undefined; @@ -21,7 +21,7 @@ declare const __SENTRY_TRACING__: boolean | undefined; * If this option is not provided, the function will use the current client's options. */ export function hasSpansEnabled( - maybeOptions?: Pick | undefined, + maybeOptions?: Pick | undefined, ): boolean { if (typeof __SENTRY_TRACING__ === 'boolean' && !__SENTRY_TRACING__) { return false; diff --git a/packages/core/src/utils/sdkMetadata.ts b/packages/core/src/utils/sdkMetadata.ts index 437668a9e7f4..714a022689e3 100644 --- a/packages/core/src/utils/sdkMetadata.ts +++ b/packages/core/src/utils/sdkMetadata.ts @@ -1,4 +1,4 @@ -import type { Options } from '../types-hoist/options'; +import type { CoreOptions } from '../types-hoist/options'; import { SDK_VERSION } from '../utils/version'; /** @@ -14,7 +14,7 @@ import { SDK_VERSION } from '../utils/version'; * @param options SDK options object that gets mutated * @param names list of package names */ -export function applySdkMetadata(options: Options, name: string, names = [name], source = 'npm'): void { +export function applySdkMetadata(options: CoreOptions, name: string, names = [name], source = 'npm'): void { const metadata = options._metadata || {}; if (!metadata.sdk) { diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index b6761c9930e7..8ff625c8dba4 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -17,7 +17,7 @@ import type { SpanStatus } from '../types-hoist/spanStatus'; import { addNonEnumerableProperty } from '../utils/object'; import { generateSpanId } from '../utils/propagationContext'; import { timestampInSeconds } from '../utils/time'; -import { generateSentryTraceHeader } from '../utils/tracing'; +import { generateSentryTraceHeader, generateTraceParentHeader } from '../utils/tracing'; import { consoleSandbox } from './debug-logger'; import { _getSpanForScope } from './spanOnScope'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 1c1912d147a5..273fb24607f0 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -10,20 +10,25 @@ import type { SerializedTraceData } from '../types-hoist/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from './baggage'; import { debug } from './debug-logger'; import { getActiveSpan, spanToTraceHeader } from './spanUtils'; -import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from './tracing'; +import { extractTraceparentData, generateSentryTraceHeader, TRACEPARENT_REGEXP } from './tracing'; /** * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation - * context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate + * context) and serializes it to `sentry-trace` and `baggage` values. These values can be used to propagate * a trace via our tracing Http headers or Html `` tags. * * This function also applies some validation to the generated sentry-trace and baggage values to ensure that * only valid strings are returned. * + * If (@param options.propagateTraceparent) is `true`, the function will also generate a `traceparent` value, + * following the W3C traceparent header format. + * * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header * or meta tag name. */ -export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { +export function getTraceData( + options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}, +): SerializedTraceData { const client = options.client || getClient(); if (!isEnabled() || !client) { return {}; @@ -47,10 +52,16 @@ export function getTraceData(options: { span?: Span; scope?: Scope; client?: Cli return {}; } - return { + const traceData: SerializedTraceData = { 'sentry-trace': sentryTrace, baggage, }; + + if (options.propagateTraceparent) { + traceData.traceparent = _sentryTraceToTraceParentHeader(sentryTrace); + } + + return traceData; } /** @@ -60,3 +71,20 @@ function scopeToTraceHeader(scope: Scope): string { const { traceId, sampled, propagationSpanId } = scope.getPropagationContext(); return generateSentryTraceHeader(traceId, propagationSpanId, sampled); } + +/** + * Builds a W3C traceparent header from the given sentry-trace header. + * + * Why parse that header and not create traceparent from primitives? + * We want these two headers to always have the same ids. The easiest way to do this is to take + * one of them as the source of truth (sentry-trace) and derive the other from it. + * + * Most importantly, this guarantees parentSpanId consistency between sentry-trace and traceparent + * in tracing without performance (TwP) mode, where we always generate a random parentSpanId. + * + * Exported for testing + */ +export function _sentryTraceToTraceParentHeader(sentryTrace: string): string { + const { traceId, parentSpanId, parentSampled } = extractTraceparentData(sentryTrace) || {}; + return `00-${traceId}-${parentSpanId}-${parentSampled ? '01' : '00'}`; +} diff --git a/packages/core/src/utils/tracing.ts b/packages/core/src/utils/tracing.ts index 509fff5acde0..0310cc8640e6 100644 --- a/packages/core/src/utils/tracing.ts +++ b/packages/core/src/utils/tracing.ts @@ -20,6 +20,9 @@ export const TRACEPARENT_REGEXP = new RegExp( /** * Extract transaction context data from a `sentry-trace` header. * + * This is terrible naming but the function has nothing to do with the W3C traceparent header. + * It can only parse the `sentry-trace` header and extract the "trace parent" data. + * * @param traceparent Traceparent string * * @returns Object containing data from the header, or undefined if traceparent string is malformed diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index d10bf1e3b592..b4eaa0ed89de 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -17,6 +17,7 @@ import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; import type { Span } from '../../../src/types-hoist/span'; import type { TestClientOptions } from '../../mocks/client'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; +import { _sentryTraceToTraceParentHeader } from '../../../src/utils/traceData'; const dsn = 'https://123@sentry.io/42'; @@ -311,4 +312,57 @@ describe('getTraceData', () => { expect(traceData).toEqual({}); }); + + it('returns traceparent from span if propagateTraceparent is true', () => { + setupClient(); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + + withActiveSpan(span, () => { + const data = getTraceData({ propagateTraceparent: true }); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + traceparent: '00-12345678901234567890123456789012-1234567890123456-01', + }); + }); + }); + + it('returns traceparent from scope in TwP config if propagateTraceparent is true', () => { + setupClient(); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampled: undefined, + sampleRand: 0.44, + }); + + const traceData = getTraceData({ propagateTraceparent: true }); + + expect(traceData.traceparent).toBeDefined(); + expect(traceData.traceparent).toMatch(/00-12345678901234567890123456789099-[0-9a-f]{16}-00/); + }); +}); + +describe('_sentryTraceToTraceParentHeader', () => { + it('returns positively sampled traceparent header for sentry-trace with positive sampling decision', () => { + const traceparent = _sentryTraceToTraceParentHeader('12345678901234567890123456789012-1234567890123456-1'); + expect(traceparent).toBe('00-12345678901234567890123456789012-1234567890123456-01'); + }); + + it('returns negatively sampled traceparent header for sentry-trace with negative sampling decision', () => { + const traceparent = _sentryTraceToTraceParentHeader('12345678901234567890123456789012-1234567890123456-0'); + expect(traceparent).toBe('00-12345678901234567890123456789012-1234567890123456-00'); + }); + + it('returns negatively sampled traceparent header for sentry-trace with no/deferred sampling decision', () => { + const traceparent = _sentryTraceToTraceParentHeader('12345678901234567890123456789012-1234567890123456'); + expect(traceparent).toBe('00-12345678901234567890123456789012-1234567890123456-00'); + }); }); diff --git a/packages/core/test/lib/utils/tracing.test.ts b/packages/core/test/lib/utils/tracing.test.ts index ea41190f3bb3..fc3b0ca9710c 100644 --- a/packages/core/test/lib/utils/tracing.test.ts +++ b/packages/core/test/lib/utils/tracing.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it, test } from 'vitest'; -import { extractTraceparentData, propagationContextFromHeaders, shouldContinueTrace } from '../../../src/utils/tracing'; +import { + extractTraceparentData, + generateTraceParentHeader, + propagationContextFromHeaders, + shouldContinueTrace, +} from '../../../src/utils/tracing'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index e200caef8e44..29a46e37da21 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -98,13 +98,9 @@ const _winterCGFetch = ((options: Partial = {}) => { return; } - instrumentFetchRequest( - handlerData, - _shouldCreateSpan, - _shouldAttachTraceData, - spans, - 'auto.http.wintercg_fetch', - ); + instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, { + spanOrigin: 'auto.http.wintercg_fetch', + }); if (breadcrumbs) { createBreadcrumb(handlerData); From 62b54937e34bebd3ffdfe883586be9fcdf6f5221 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 2 Sep 2025 18:21:32 +0200 Subject: [PATCH 2/7] fix import --- packages/core/src/utils/spanUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 8ff625c8dba4..b6761c9930e7 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -17,7 +17,7 @@ import type { SpanStatus } from '../types-hoist/spanStatus'; import { addNonEnumerableProperty } from '../utils/object'; import { generateSpanId } from '../utils/propagationContext'; import { timestampInSeconds } from '../utils/time'; -import { generateSentryTraceHeader, generateTraceParentHeader } from '../utils/tracing'; +import { generateSentryTraceHeader } from '../utils/tracing'; import { consoleSandbox } from './debug-logger'; import { _getSpanForScope } from './spanOnScope'; From 11fe3c5df179e49d4d133556928a3414d1d64b0f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 2 Sep 2025 18:23:21 +0200 Subject: [PATCH 3/7] fix cloudflare test --- packages/cloudflare/test/integrations/fetch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/test/integrations/fetch.test.ts b/packages/cloudflare/test/integrations/fetch.test.ts index 2a8f9cf6e718..03cdbb9bf5a5 100644 --- a/packages/cloudflare/test/integrations/fetch.test.ts +++ b/packages/cloudflare/test/integrations/fetch.test.ts @@ -58,7 +58,7 @@ describe('WinterCGFetch instrumentation', () => { expect.any(Function), expect.any(Function), expect.any(Object), - 'auto.http.fetch', + { spanOrigin: 'auto.http.fetch' }, ); const [, shouldCreateSpan, shouldAttachTraceData] = instrumentFetchRequestSpy.mock.calls[0]!; From c7b06c18ca0e19686babbcefaa3ead6f9899c0cd Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 3 Sep 2025 14:04:32 +0200 Subject: [PATCH 4/7] fix lint and failing tests --- packages/browser/test/tracing/request.test.ts | 4 ++++ packages/core/test/lib/utils/traceData.test.ts | 2 +- packages/core/test/lib/utils/tracing.test.ts | 7 +------ packages/vercel-edge/test/wintercg-fetch.test.ts | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index 298a2693d096..1674a96d1937 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -17,6 +17,10 @@ class MockClient implements Partial { // Mock addEventProcessor function this.addEventProcessor = vi.fn(); } + // @ts-expect-error not returning options for the test + public getOptions() { + return {}; + } } describe('instrumentOutgoingRequests', () => { diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index b4eaa0ed89de..5bcec5948b31 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -15,9 +15,9 @@ import { import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; import type { Span } from '../../../src/types-hoist/span'; +import { _sentryTraceToTraceParentHeader } from '../../../src/utils/traceData'; import type { TestClientOptions } from '../../mocks/client'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; -import { _sentryTraceToTraceParentHeader } from '../../../src/utils/traceData'; const dsn = 'https://123@sentry.io/42'; diff --git a/packages/core/test/lib/utils/tracing.test.ts b/packages/core/test/lib/utils/tracing.test.ts index fc3b0ca9710c..ea41190f3bb3 100644 --- a/packages/core/test/lib/utils/tracing.test.ts +++ b/packages/core/test/lib/utils/tracing.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it, test } from 'vitest'; -import { - extractTraceparentData, - generateTraceParentHeader, - propagationContextFromHeaders, - shouldContinueTrace, -} from '../../../src/utils/tracing'; +import { extractTraceparentData, propagationContextFromHeaders, shouldContinueTrace } from '../../../src/utils/tracing'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index 7dc67d0131ea..cb5506a71e05 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -59,7 +59,7 @@ describe('WinterCGFetch instrumentation', () => { expect.any(Function), expect.any(Function), expect.any(Object), - 'auto.http.wintercg_fetch', + { spanOrigin: 'auto.http.wintercg_fetch' }, ); const [, shouldCreateSpan, shouldAttachTraceData] = instrumentFetchRequestSpy.mock.calls[0]!; From af4a72dd9abec4c3319e8b9768b158a56e54ee3b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 3 Sep 2025 14:11:26 +0200 Subject: [PATCH 5/7] fix error in test, size limit --- .size-limit.js | 6 +++--- .../suites/tracing/request/fetch/test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 3ea2bdf80703..ee0d2804558f 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -75,7 +75,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '83 KB', + limit: '84 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -120,7 +120,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '42 KB', + limit: '43 KB', }, // Vue SDK (ESM) { @@ -206,7 +206,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '44 KB', + limit: '45 KB', }, // SvelteKit SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index 4253f7b6e109..72a196d5d8e9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -75,7 +75,7 @@ sentryTest('attaches `sentry-trace` and `baggage` headers to fetch requests', as baggage: expect.any(String), 'x-test-header': 'existing-header', }); - expect(requestHeaders1).not.toHaveProperty('traceparent'); + expect(requestHeaders2).not.toHaveProperty('traceparent'); const request3 = requests[2]; const requestHeaders3 = request3.headers(); @@ -83,5 +83,5 @@ sentryTest('attaches `sentry-trace` and `baggage` headers to fetch requests', as 'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/), baggage: expect.any(String), }); - expect(requestHeaders1).not.toHaveProperty('traceparent'); + expect(requestHeaders3).not.toHaveProperty('traceparent'); }); From 3dca058e11fecf64c1a5b0f4834934d7971d5179 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 4 Sep 2025 10:29:39 +0200 Subject: [PATCH 6/7] fix `_sentryTraceToTraceParentHeader` potentially returning invalid traceparent header --- packages/core/src/utils/traceData.ts | 10 ++++++++-- packages/core/test/lib/utils/traceData.test.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 273fb24607f0..aa335dbd37bd 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -58,7 +58,10 @@ export function getTraceData( }; if (options.propagateTraceparent) { - traceData.traceparent = _sentryTraceToTraceParentHeader(sentryTrace); + const traceparent = _sentryTraceToTraceParentHeader(sentryTrace); + if (traceparent) { + traceData.traceparent = traceparent; + } } return traceData; @@ -84,7 +87,10 @@ function scopeToTraceHeader(scope: Scope): string { * * Exported for testing */ -export function _sentryTraceToTraceParentHeader(sentryTrace: string): string { +export function _sentryTraceToTraceParentHeader(sentryTrace: string): string | undefined { const { traceId, parentSpanId, parentSampled } = extractTraceparentData(sentryTrace) || {}; + if (!traceId || !parentSpanId) { + return undefined; + } return `00-${traceId}-${parentSpanId}-${parentSampled ? '01' : '00'}`; } diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index 5bcec5948b31..37764cfc94a3 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -365,4 +365,18 @@ describe('_sentryTraceToTraceParentHeader', () => { const traceparent = _sentryTraceToTraceParentHeader('12345678901234567890123456789012-1234567890123456'); expect(traceparent).toBe('00-12345678901234567890123456789012-1234567890123456-00'); }); + + it.each([ + '12345678901234567890123456789012--0', + '-12345678901234567890123456789012-0', + '--1', + '0', + '1', + '', + '00-12345678901234567890123456789012-1234567890123456-01', + '00-12345678901234567890123456789012-1234567890123456-00', + ])('returns undefined if the sentry-trace header is invalid (%s)', sentryTrace => { + const traceparent = _sentryTraceToTraceParentHeader(sentryTrace); + expect(traceparent).toBeUndefined(); + }); }); From 446f853f5b9656c1c5eb2f55556f981b5e32eeff Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 4 Sep 2025 10:39:28 +0200 Subject: [PATCH 7/7] avoid exporting xhrCallback --- packages/browser/src/tracing/request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 7be890c31313..d046793b42a1 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -310,7 +310,7 @@ export function shouldAttachHeaders( * * @returns Span if a span was created, otherwise void. */ -export function xhrCallback( +function xhrCallback( handlerData: HandlerDataXhr, shouldCreateSpan: (url: string) => boolean, shouldAttachHeaders: (url: string) => boolean,