diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/subject.js new file mode 100644 index 000000000000..584c264d1f0a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/subject.js @@ -0,0 +1,7 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://sentry-test-site.example/1'); +xhr.setRequestHeader('X-Test-Header', 'existing-header'); +xhr.setRequestHeader('baggage', 'someVendor-foo=bar'); + +xhr.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/test.ts new file mode 100644 index 000000000000..3086347cbd0c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-merged-baggage-headers/test.ts @@ -0,0 +1,25 @@ +import { expect } from '@playwright/test'; +import { TRACEPARENT_REGEXP } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('merges `baggage` headers of pre-existing non-sentry XHR requests', 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': expect.stringMatching(TRACEPARENT_REGEXP), + baggage: expect.stringMatching(/^someVendor-foo=bar, sentry-.*$/), + 'x-test-header': 'existing-header', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/subject.js new file mode 100644 index 000000000000..595ab4b67bac --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/subject.js @@ -0,0 +1,8 @@ +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.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/test.ts new file mode 100644 index 000000000000..49d4b3091258 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers/test.ts @@ -0,0 +1,27 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'attaches manually passed in `sentry-trace` and `baggage` headers to XHR requests', + 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', + }); + }, +); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 4fb816d1319d..cc987fca2966 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -420,21 +420,38 @@ function setHeaderOnXhr( sentryTraceHeader: string, sentryBaggageHeader: string | undefined, ): void { + const originalHeaders = xhr.__sentry_xhr_v3__?.request_headers; + + if (originalHeaders?.['sentry-trace']) { + // 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); if (sentryBaggageHeader) { - // 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); + // only add our headers if + // - no pre-existing baggage header exists + // - or it is set and doesn't yet contain sentry values + const originalBaggageHeader = originalHeaders?.['baggage']; + if (!originalBaggageHeader || !baggageHeaderHasSentryValues(originalBaggageHeader)) { + // 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); + } } } catch (_) { // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. } } +function baggageHeaderHasSentryValues(baggageHeader: string): boolean { + return baggageHeader.split(',').some(value => value.trim().startsWith('sentry-')); +} + function getFullURL(url: string): string | undefined { try { // By adding a base URL to new URL(), this will also work for relative urls