Skip to content

Commit 69224b8

Browse files
authored
fix(core): Respect manually set sentry tracing headers in fetch calls (#16183)
If users manually pass in `sentry-trace` and `baggage` headers, we previously overwrote the values with the ones we computed. With this patch, we: - still check for pre-existing sentry headers, but do nothing if we find them instead of replacing them - still ensure that we merge pre-existing non-sentry baggage values with our baggage values - add a bunch of unit tests for the various header and request types (I feel like I wrote these before but couldn't find em) - add an integration test to demonstrate that manually passed in headers are still taken
1 parent 640b57f commit 69224b8

File tree

9 files changed

+501
-60
lines changed

9 files changed

+501
-60
lines changed

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/trace-header-merging/test.ts renamed to dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/test.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ async function assertRequests({
3232
});
3333
});
3434

35+
expect(requests).toHaveLength(2);
36+
3537
requests.forEach(request => {
3638
const headers = request.headers();
3739

3840
// No merged sentry trace headers
3941
expect(headers['sentry-trace']).not.toContain(',');
4042

4143
// No multiple baggage entries
42-
expect(headers['baggage'].match(/sentry-trace_id/g) ?? []).toHaveLength(1);
44+
expect(headers['baggage'].match(/sentry-release/g) ?? []).toHaveLength(1);
4345
});
4446
}
4547

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [Sentry.browserTracingIntegration()],
8+
tracePropagationTargets: ['http://sentry-test-site.example'],
9+
tracesSampleRate: 1,
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fetch('http://sentry-test-site.example/api/test/', {
2+
headers: { 'sentry-trace': 'abc-123-1', baggage: 'sentry-trace_id=abc' },
3+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { shouldSkipTracingTest } from '../../../../utils/helpers';
4+
5+
sentryTest("instrumentation doesn't override manually added sentry headers", async ({ getLocalTestUrl, page }) => {
6+
if (shouldSkipTracingTest()) {
7+
sentryTest.skip();
8+
}
9+
10+
const requestPromise = page.waitForRequest('http://sentry-test-site.example/api/test/');
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
await page.goto(url);
15+
16+
const request = await requestPromise;
17+
18+
const headers = await request.allHeaders();
19+
20+
expect(headers['sentry-trace']).toBe('abc-123-1');
21+
expect(headers.baggage).toBe('sentry-trace_id=abc');
22+
});

packages/core/src/fetch.ts

+52-59
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,15 @@ export function instrumentFetchRequest(
103103

104104
/**
105105
* Adds sentry-trace and baggage headers to the various forms of fetch headers.
106+
* exported only for testing purposes
107+
*
108+
* When we determine if we should add a baggage header, there are 3 cases:
109+
* 1. No previous baggage header -> add baggage
110+
* 2. Previous baggage header has no sentry baggage values -> add our baggage
111+
* 3. Previous baggage header has sentry baggage values -> do nothing (might have been added manually by users)
106112
*/
107-
function _addTracingHeadersToFetchRequest(
113+
// eslint-disable-next-line complexity -- yup it's this complicated :(
114+
export function _addTracingHeadersToFetchRequest(
108115
request: string | Request,
109116
fetchOptionsObj: {
110117
headers?:
@@ -124,78 +131,70 @@ function _addTracingHeadersToFetchRequest(
124131
return undefined;
125132
}
126133

127-
const headers = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined);
134+
const originalHeaders = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined);
128135

129-
if (!headers) {
136+
if (!originalHeaders) {
130137
return { ...traceHeaders };
131-
} else if (isHeaders(headers)) {
132-
const newHeaders = new Headers(headers);
133-
newHeaders.set('sentry-trace', sentryTrace);
138+
} else if (isHeaders(originalHeaders)) {
139+
const newHeaders = new Headers(originalHeaders);
140+
141+
// We don't want to override manually added sentry headers
142+
if (!newHeaders.get('sentry-trace')) {
143+
newHeaders.set('sentry-trace', sentryTrace);
144+
}
134145

135146
if (baggage) {
136147
const prevBaggageHeader = newHeaders.get('baggage');
137-
if (prevBaggageHeader) {
138-
const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader);
139-
newHeaders.set(
140-
'baggage',
141-
// If there are non-sentry entries (i.e. if the stripped string is non-empty/truthy) combine the stripped header and sentry baggage header
142-
// otherwise just set the sentry baggage header
143-
prevHeaderStrippedFromSentryBaggage ? `${prevHeaderStrippedFromSentryBaggage},${baggage}` : baggage,
144-
);
145-
} else {
148+
149+
if (!prevBaggageHeader) {
146150
newHeaders.set('baggage', baggage);
151+
} else if (!baggageHeaderHasSentryBaggageValues(prevBaggageHeader)) {
152+
newHeaders.set('baggage', `${prevBaggageHeader},${baggage}`);
147153
}
148154
}
149155

150156
return newHeaders;
151-
} else if (Array.isArray(headers)) {
152-
const newHeaders = [
153-
...headers
154-
// Remove any existing sentry-trace headers
155-
.filter(header => {
156-
return !(Array.isArray(header) && header[0] === 'sentry-trace');
157-
})
158-
// Get rid of previous sentry baggage values in baggage header
159-
.map(header => {
160-
if (Array.isArray(header) && header[0] === 'baggage' && typeof header[1] === 'string') {
161-
const [headerName, headerValue, ...rest] = header;
162-
return [headerName, stripBaggageHeaderOfSentryBaggageValues(headerValue), ...rest];
163-
} else {
164-
return header;
165-
}
166-
}),
167-
// Attach the new sentry-trace header
168-
['sentry-trace', sentryTrace],
169-
];
157+
} else if (Array.isArray(originalHeaders)) {
158+
const newHeaders = [...originalHeaders];
170159

171-
if (baggage) {
160+
if (!originalHeaders.find(header => header[0] === 'sentry-trace')) {
161+
newHeaders.push(['sentry-trace', sentryTrace]);
162+
}
163+
164+
const prevBaggageHeaderWithSentryValues = originalHeaders.find(
165+
header => header[0] === 'baggage' && baggageHeaderHasSentryBaggageValues(header[1]),
166+
);
167+
168+
if (baggage && !prevBaggageHeaderWithSentryValues) {
172169
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
173170
// Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header.
174171
newHeaders.push(['baggage', baggage]);
175172
}
176173

177174
return newHeaders as PolymorphicRequestHeaders;
178175
} else {
179-
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
180-
let newBaggageHeaders: string[] = [];
181-
182-
if (Array.isArray(existingBaggageHeader)) {
183-
newBaggageHeaders = existingBaggageHeader
184-
.map(headerItem =>
185-
typeof headerItem === 'string' ? stripBaggageHeaderOfSentryBaggageValues(headerItem) : headerItem,
186-
)
187-
.filter(headerItem => headerItem === '');
188-
} else if (existingBaggageHeader) {
189-
newBaggageHeaders.push(stripBaggageHeaderOfSentryBaggageValues(existingBaggageHeader));
190-
}
191-
192-
if (baggage) {
176+
const existingSentryTraceHeader = 'sentry-trace' in originalHeaders ? originalHeaders['sentry-trace'] : undefined;
177+
178+
const existingBaggageHeader = 'baggage' in originalHeaders ? originalHeaders.baggage : undefined;
179+
const newBaggageHeaders: string[] = existingBaggageHeader
180+
? Array.isArray(existingBaggageHeader)
181+
? [...existingBaggageHeader]
182+
: [existingBaggageHeader]
183+
: [];
184+
185+
const prevBaggageHeaderWithSentryValues =
186+
existingBaggageHeader &&
187+
(Array.isArray(existingBaggageHeader)
188+
? existingBaggageHeader.find(headerItem => baggageHeaderHasSentryBaggageValues(headerItem))
189+
: baggageHeaderHasSentryBaggageValues(existingBaggageHeader));
190+
191+
if (baggage && !prevBaggageHeaderWithSentryValues) {
193192
newBaggageHeaders.push(baggage);
194193
}
195194

196195
return {
197-
...(headers as Exclude<typeof headers, Headers>),
198-
'sentry-trace': sentryTrace,
196+
...(originalHeaders as Exclude<typeof originalHeaders, Headers>),
197+
'sentry-trace': (existingSentryTraceHeader as string | undefined) ?? sentryTrace,
199198
baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined,
200199
};
201200
}
@@ -219,14 +218,8 @@ function endSpan(span: Span, handlerData: HandlerDataFetch): void {
219218
span.end();
220219
}
221220

222-
function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string {
223-
return (
224-
baggageHeader
225-
.split(',')
226-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
227-
.filter(baggageEntry => !baggageEntry.split('=')[0]!.startsWith(SENTRY_BAGGAGE_KEY_PREFIX))
228-
.join(',')
229-
);
221+
function baggageHeaderHasSentryBaggageValues(baggageHeader: string): boolean {
222+
return baggageHeader.split(',').some(baggageEntry => baggageEntry.trim().startsWith(SENTRY_BAGGAGE_KEY_PREFIX));
230223
}
231224

232225
function isHeaders(headers: unknown): headers is Headers {

0 commit comments

Comments
 (0)