diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/template.html rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/template.html diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/test.ts similarity index 94% rename from dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/test.ts index 18f37a5c9c28..7c0f6db3483b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/test.ts @@ -32,6 +32,8 @@ async function assertRequests({ }); }); + expect(requests).toHaveLength(2); + requests.forEach(request => { const headers = request.headers(); @@ -39,7 +41,7 @@ async function assertRequests({ expect(headers['sentry-trace']).not.toContain(','); // No multiple baggage entries - expect(headers['baggage'].match(/sentry-trace_id/g) ?? []).toHaveLength(1); + expect(headers['baggage'].match(/sentry-release/g) ?? []).toHaveLength(1); }); } diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/init.js new file mode 100644 index 000000000000..b2280b70e307 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/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'], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/subject.js new file mode 100644 index 000000000000..6301cb2916a4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/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' }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/test.ts new file mode 100644 index 000000000000..226a791f74bd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest("instrumentation doesn't override manually added sentry headers", 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'); +}); diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index a7df2ca147f2..65274d1e82a3 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -103,8 +103,15 @@ export function instrumentFetchRequest( /** * Adds sentry-trace and baggage headers to the various forms of fetch headers. + * exported only for testing purposes + * + * When we determine if we should add a baggage header, there are 3 cases: + * 1. No previous baggage header -> add baggage + * 2. Previous baggage header has no sentry baggage values -> add our baggage + * 3. Previous baggage header has sentry baggage values -> do nothing (might have been added manually by users) */ -function _addTracingHeadersToFetchRequest( +// eslint-disable-next-line complexity -- yup it's this complicated :( +export function _addTracingHeadersToFetchRequest( request: string | Request, fetchOptionsObj: { headers?: @@ -124,51 +131,41 @@ function _addTracingHeadersToFetchRequest( return undefined; } - const headers = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined); + const originalHeaders = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined); - if (!headers) { + if (!originalHeaders) { return { ...traceHeaders }; - } else if (isHeaders(headers)) { - const newHeaders = new Headers(headers); - newHeaders.set('sentry-trace', sentryTrace); + } else if (isHeaders(originalHeaders)) { + const newHeaders = new Headers(originalHeaders); + + // We don't want to override manually added sentry headers + if (!newHeaders.get('sentry-trace')) { + newHeaders.set('sentry-trace', sentryTrace); + } if (baggage) { const prevBaggageHeader = newHeaders.get('baggage'); - if (prevBaggageHeader) { - const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader); - newHeaders.set( - 'baggage', - // If there are non-sentry entries (i.e. if the stripped string is non-empty/truthy) combine the stripped header and sentry baggage header - // otherwise just set the sentry baggage header - prevHeaderStrippedFromSentryBaggage ? `${prevHeaderStrippedFromSentryBaggage},${baggage}` : baggage, - ); - } else { + + if (!prevBaggageHeader) { newHeaders.set('baggage', baggage); + } else if (!baggageHeaderHasSentryBaggageValues(prevBaggageHeader)) { + newHeaders.set('baggage', `${prevBaggageHeader},${baggage}`); } } return newHeaders; - } else if (Array.isArray(headers)) { - const newHeaders = [ - ...headers - // Remove any existing sentry-trace headers - .filter(header => { - return !(Array.isArray(header) && header[0] === 'sentry-trace'); - }) - // Get rid of previous sentry baggage values in baggage header - .map(header => { - if (Array.isArray(header) && header[0] === 'baggage' && typeof header[1] === 'string') { - const [headerName, headerValue, ...rest] = header; - return [headerName, stripBaggageHeaderOfSentryBaggageValues(headerValue), ...rest]; - } else { - return header; - } - }), - // Attach the new sentry-trace header - ['sentry-trace', sentryTrace], - ]; + } else if (Array.isArray(originalHeaders)) { + const newHeaders = [...originalHeaders]; - if (baggage) { + if (!originalHeaders.find(header => header[0] === 'sentry-trace')) { + newHeaders.push(['sentry-trace', sentryTrace]); + } + + const prevBaggageHeaderWithSentryValues = originalHeaders.find( + header => header[0] === 'baggage' && baggageHeaderHasSentryBaggageValues(header[1]), + ); + + if (baggage && !prevBaggageHeaderWithSentryValues) { // If there are multiple entries with the same key, the browser will merge the values into a single request header. // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. newHeaders.push(['baggage', baggage]); @@ -176,26 +173,28 @@ function _addTracingHeadersToFetchRequest( return newHeaders as PolymorphicRequestHeaders; } else { - const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined; - let newBaggageHeaders: string[] = []; - - if (Array.isArray(existingBaggageHeader)) { - newBaggageHeaders = existingBaggageHeader - .map(headerItem => - typeof headerItem === 'string' ? stripBaggageHeaderOfSentryBaggageValues(headerItem) : headerItem, - ) - .filter(headerItem => headerItem === ''); - } else if (existingBaggageHeader) { - newBaggageHeaders.push(stripBaggageHeaderOfSentryBaggageValues(existingBaggageHeader)); - } - - if (baggage) { + const existingSentryTraceHeader = 'sentry-trace' in originalHeaders ? originalHeaders['sentry-trace'] : undefined; + + const existingBaggageHeader = 'baggage' in originalHeaders ? originalHeaders.baggage : undefined; + const newBaggageHeaders: string[] = existingBaggageHeader + ? Array.isArray(existingBaggageHeader) + ? [...existingBaggageHeader] + : [existingBaggageHeader] + : []; + + const prevBaggageHeaderWithSentryValues = + existingBaggageHeader && + (Array.isArray(existingBaggageHeader) + ? existingBaggageHeader.find(headerItem => baggageHeaderHasSentryBaggageValues(headerItem)) + : baggageHeaderHasSentryBaggageValues(existingBaggageHeader)); + + if (baggage && !prevBaggageHeaderWithSentryValues) { newBaggageHeaders.push(baggage); } return { - ...(headers as Exclude), - 'sentry-trace': sentryTrace, + ...(originalHeaders as Exclude), + 'sentry-trace': (existingSentryTraceHeader as string | undefined) ?? sentryTrace, baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined, }; } @@ -219,14 +218,8 @@ function endSpan(span: Span, handlerData: HandlerDataFetch): void { span.end(); } -function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string { - return ( - baggageHeader - .split(',') - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .filter(baggageEntry => !baggageEntry.split('=')[0]!.startsWith(SENTRY_BAGGAGE_KEY_PREFIX)) - .join(',') - ); +function baggageHeaderHasSentryBaggageValues(baggageHeader: string): boolean { + return baggageHeader.split(',').some(baggageEntry => baggageEntry.trim().startsWith(SENTRY_BAGGAGE_KEY_PREFIX)); } function isHeaders(headers: unknown): headers is Headers { diff --git a/packages/core/test/lib/fetch.test.ts b/packages/core/test/lib/fetch.test.ts new file mode 100644 index 000000000000..cafc22a562c8 --- /dev/null +++ b/packages/core/test/lib/fetch.test.ts @@ -0,0 +1,411 @@ +import { describe, expect, it, vi } from 'vitest'; +import { _addTracingHeadersToFetchRequest } from '../../src/fetch'; + +const { DEFAULT_SENTRY_TRACE, DEFAULT_BAGGAGE } = vi.hoisted(() => ({ + DEFAULT_SENTRY_TRACE: 'defaultTraceId-defaultSpanId-1', + DEFAULT_BAGGAGE: 'sentry-trace_id=defaultTraceId,sentry-sampled=true,sentry-sample_rate=0.5,sentry-sample_rand=0.232', +})); + +const CUSTOM_SENTRY_TRACE = '123-abc-1'; +// adding in random spaces here to ensure they are trimmed and our sentry baggage item detection logic works. +// Spaces between items are allowed by the baggage spec. +const CUSTOM_BAGGAGE = ' sentry-trace_id=123 , sentry-sampled=true'; + +vi.mock('../../src/utils/traceData', () => { + return { + getTraceData: vi.fn(() => { + return { + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + }; + }), + }; +}); + +describe('_addTracingHeadersToFetchRequest', () => { + describe('when request is a string', () => { + describe('and no request headers are set', () => { + it.each([ + { + options: {}, + }, + { + options: { headers: {} }, + }, + ])('attaches sentry headers (options: $options)', ({ options }) => { + expect(_addTracingHeadersToFetchRequest('/api/test', options)).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + }); + }); + }); + + describe('and request headers are set in options', () => { + it('attaches sentry headers to headers object', () => { + expect(_addTracingHeadersToFetchRequest('/api/test', { headers: { 'custom-header': 'custom-value' } })).toEqual( + { + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + 'custom-header': 'custom-value', + }, + ); + }); + + it('attaches sentry headers to a Headers instance', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: new Headers({ 'custom-header': 'custom-value' }), + }); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + 'custom-header': 'custom-value', + }); + }); + + it('attaches sentry headers to headers array', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: [['custom-header', 'custom-value']], + }); + + expect(Array.isArray(returnedHeaders)).toBe(true); + expect(returnedHeaders).toEqual([ + ['custom-header', 'custom-value'], + ['sentry-trace', DEFAULT_SENTRY_TRACE], + ['baggage', DEFAULT_BAGGAGE], + ]); + }); + }); + + describe('and 3rd party baggage header is set', () => { + it('adds additional sentry baggage values to Headers instance', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: new Headers({ + baggage: 'custom-baggage=1,someVal=bar', + }), + }); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,${DEFAULT_BAGGAGE}`, + }); + }); + + it('adds additional sentry baggage values to headers array', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: [['baggage', 'custom-baggage=1,someVal=bar']], + }); + + expect(Array.isArray(returnedHeaders)).toBe(true); + + expect(returnedHeaders).toEqual([ + ['baggage', 'custom-baggage=1,someVal=bar'], + ['sentry-trace', DEFAULT_SENTRY_TRACE], + ['baggage', DEFAULT_BAGGAGE], + ]); + }); + + it('adds additional sentry baggage values to headers object', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: { + baggage: 'custom-baggage=1,someVal=bar', + }, + }); + + expect(typeof returnedHeaders).toBe('object'); + + expect(returnedHeaders).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,${DEFAULT_BAGGAGE}`, + }); + }); + + it('adds additional sentry baggage values to headers object with arrays', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: { + baggage: ['custom-baggage=1,someVal=bar', 'other-vendor-key=value'], + }, + }); + + expect(typeof returnedHeaders).toBe('object'); + + expect(returnedHeaders).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,other-vendor-key=value,${DEFAULT_BAGGAGE}`, + }); + }); + }); + + describe('and Sentry values are already set', () => { + it('does not override them (Headers instance)', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: new Headers({ + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE, + 'custom-header': 'custom-value', + }), + }); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'custom-header': 'custom-value', + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE.trim(), + }); + }); + + it('does not override them (headers array)', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: [ + ['sentry-trace', CUSTOM_SENTRY_TRACE], + ['baggage', CUSTOM_BAGGAGE], + ['custom-header', 'custom-value'], + ], + }); + + expect(Array.isArray(returnedHeaders)).toBe(true); + + expect(returnedHeaders).toEqual([ + ['sentry-trace', CUSTOM_SENTRY_TRACE], + ['baggage', CUSTOM_BAGGAGE], + ['custom-header', 'custom-value'], + ]); + }); + + it('does not override them (headers object)', () => { + const returnedHeaders = _addTracingHeadersToFetchRequest('/api/test', { + headers: { + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE, + 'custom-header': 'custom-value', + }, + }); + + expect(typeof returnedHeaders).toBe('object'); + + expect(returnedHeaders).toEqual({ + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE, + 'custom-header': 'custom-value', + }); + }); + }); + }); + + describe('when request is a Request instance', () => { + describe('and no request headers are set', () => { + it('attaches sentry headers', () => { + const request = new Request('http://locahlost:3000/api/test'); + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + }); + }); + }); + + describe('and request headers are set in options', () => { + it('attaches sentry headers to headers instance', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: new Headers({ 'custom-header': 'custom-value' }), + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + 'custom-header': 'custom-value', + }); + }); + + it('attaches sentry headers to headers object', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: { 'custom-header': 'custom-value' }, + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + 'custom-header': 'custom-value', + }); + }); + + it('attaches sentry headers to headers array', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: [['custom-header', 'custom-value']], + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: DEFAULT_BAGGAGE, + 'custom-header': 'custom-value', + }); + }); + }); + + describe('and 3rd party baggage header is set', () => { + it('adds additional sentry baggage values to Headers instance', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: new Headers({ + baggage: 'custom-baggage=1,someVal=bar', + 'custom-header': 'custom-value', + }), + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'custom-header': 'custom-value', + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,${DEFAULT_BAGGAGE}`, + }); + }); + + it('adds additional sentry baggage values to headers array', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: [['baggage', 'custom-baggage=1,someVal=bar']], + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,${DEFAULT_BAGGAGE}`, + }); + }); + + it('adds additional sentry baggage values to headers object', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: { + baggage: 'custom-baggage=1,someVal=bar', + }, + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,${DEFAULT_BAGGAGE}`, + }); + }); + + it('adds additional sentry baggage values to headers object with arrays', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: { + baggage: ['custom-baggage=1,someVal=bar', 'other-vendor-key=value'], + }, + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'sentry-trace': DEFAULT_SENTRY_TRACE, + baggage: `custom-baggage=1,someVal=bar,other-vendor-key=value,${DEFAULT_BAGGAGE}`, + }); + }); + }); + + describe('and Sentry values are already set', () => { + it('does not override them (Headers instance)', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: new Headers({ + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE, + 'custom-header': 'custom-value', + }), + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'custom-header': 'custom-value', + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE.trim(), + }); + }); + + it('does not override them (headers array)', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: [ + ['sentry-trace', CUSTOM_SENTRY_TRACE], + ['baggage', CUSTOM_BAGGAGE], + ['custom-header', 'custom-value'], + ], + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'custom-header': 'custom-value', + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE.trim(), + }); + }); + + it('does not override them (headers object)', () => { + const request = new Request('http://locahlost:3000/api/test', { + headers: { + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE, + 'custom-header': 'custom-value', + }, + }); + + const returnedHeaders = _addTracingHeadersToFetchRequest(request, {}); + + expect(returnedHeaders).toBeInstanceOf(Headers); + + // @ts-expect-error -- we know it's a Headers instance and entries() exists + expect(Object.fromEntries(returnedHeaders!.entries())).toEqual({ + 'custom-header': 'custom-value', + 'sentry-trace': CUSTOM_SENTRY_TRACE, + baggage: CUSTOM_BAGGAGE.trim(), + }); + }); + }); + }); +});