Skip to content
Merged
4 changes: 2 additions & 2 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '78 KB',
limit: '78.1 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
Expand Down Expand Up @@ -138,7 +138,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
ignore: ['react/jsx-runtime'],
gzip: true,
limit: '39 KB',
limit: '39.05 KB',
},
// Vue SDK (ESM)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://example.com'],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
fetchPojo.addEventListener('click', () => {
const fetchOptions = {
headers: {
'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'sentry-release=4.2.0',
},
};

// Make two fetch requests that reuse the same fetch object
Sentry.startSpan({ name: 'does-not-matter-1' }, () =>
fetch('http://example.com/fetch-pojo', fetchOptions)
.then(res => res.text())
.then(() =>
Sentry.startSpan({ name: 'does-not-matter-2' }, () => fetch('http://example.com/fetch-pojo', fetchOptions)),
),
);
});

fetchArray.addEventListener('click', () => {
const fetchOptions = {
headers: [
['sentry-trace', '12312012123120121231201212312012-1121201211212012-1'],
['baggage', 'sentry-release=4.2.0'],
],
};

// Make two fetch requests that reuse the same fetch object
Sentry.startSpan({ name: 'does-not-matter-1' }, () =>
fetch('http://example.com/fetch-array', fetchOptions)
.then(res => res.text())
.then(() =>
Sentry.startSpan({ name: 'does-not-matter-2' }, () => fetch('http://example.com/fetch-array', fetchOptions)),
),
);
});

fetchHeaders.addEventListener('click', () => {
const fetchOptions = {
headers: new Headers({
'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
baggage: 'sentry-release=4.2.0',
}),
};

// Make two fetch requests that reuse the same fetch object
Sentry.startSpan({ name: 'does-not-matter-1' }, () =>
fetch('http://example.com/fetch-headers', fetchOptions)
.then(res => res.text())
.then(() =>
Sentry.startSpan({ name: 'does-not-matter-2' }, () => fetch('http://example.com/fetch-headers', fetchOptions)),
),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="fetchPojo">Fetch POJO</button>
<button id="fetchArray">Fetch array</button>
<button id="fetchHeaders">Fetch Headers</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Page, Request } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';

async function assertRequests({
page,
buttonSelector,
requestMatcher,
}: { page: Page; buttonSelector: string; requestMatcher: string }) {
const requests = await new Promise<Request[]>(resolve => {
const requests: Request[] = [];
page
.route(requestMatcher, (route, request) => {
requests.push(request);
if (requests.length === 2) {
resolve(requests);
}

return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({}),
});
})
.then(() => {
page.click(buttonSelector);
});
});

requests.forEach(request => {
const headers = request.headers();

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

// No multiple baggage entries
expect(headers['baggage'].match(/sentry-trace_id/g) ?? []).toHaveLength(1);
});
}

sentryTest(
'Ensure the SDK does not infinitely append tracing headers to outgoing requests',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

await sentryTest.step('fetch with POJO', () =>
assertRequests({ page, buttonSelector: '#fetchPojo', requestMatcher: 'http://example.com/fetch-pojo' }),
);

await sentryTest.step('fetch with array', () =>
assertRequests({ page, buttonSelector: '#fetchArray', requestMatcher: 'http://example.com/fetch-array' }),
);

await sentryTest.step('fetch with Headers instance', () =>
assertRequests({ page, buttonSelector: '#fetchHeaders', requestMatcher: 'http://example.com/fetch-headers' }),
);
},
);
63 changes: 53 additions & 10 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types';
import {
BAGGAGE_HEADER_NAME,
SENTRY_BAGGAGE_KEY_PREFIX,
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
isInstanceOf,
Expand Down Expand Up @@ -122,7 +123,7 @@ export function addTracingHeadersToFetchRequest(
request: string | unknown, // unknown is actually type Request but we can't export DOM types from this package,
client: Client,
scope: Scope,
options: {
fetchOptionsObj: {
headers?:
| {
[key: string]: string[] | string | undefined;
Expand All @@ -145,25 +146,53 @@ export function addTracingHeadersToFetchRequest(
);

const headers =
options.headers ||
fetchOptionsObj.headers ||
(typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : undefined);

if (!headers) {
return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader };
} else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) {
const newHeaders = new Headers(headers as Headers);

newHeaders.append('sentry-trace', sentryTraceHeader);
newHeaders.set('sentry-trace', sentryTraceHeader);

if (sentryBaggageHeader) {
// If the same header is appended multiple times 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.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
const prevBaggageHeader = newHeaders.get(BAGGAGE_HEADER_NAME);
if (prevBaggageHeader) {
const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader);
newHeaders.set(
BAGGAGE_HEADER_NAME,
// 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},${sentryBaggageHeader}`
: sentryBaggageHeader,
);
} else {
newHeaders.set(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
}
}

return newHeaders as PolymorphicRequestHeaders;
} else if (Array.isArray(headers)) {
const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]];
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_HEADER_NAME && 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', sentryTraceHeader],
];

if (sentryBaggageHeader) {
// If there are multiple entries with the same key, the browser will merge the values into a single request header.
Expand All @@ -174,12 +203,16 @@ export function addTracingHeadersToFetchRequest(
return newHeaders as PolymorphicRequestHeaders;
} else {
const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined;
const newBaggageHeaders: string[] = [];
let newBaggageHeaders: string[] = [];

if (Array.isArray(existingBaggageHeader)) {
newBaggageHeaders.push(...existingBaggageHeader);
newBaggageHeaders = existingBaggageHeader
.map(headerItem =>
typeof headerItem === 'string' ? stripBaggageHeaderOfSentryBaggageValues(headerItem) : headerItem,
)
.filter(headerItem => headerItem === '');
} else if (existingBaggageHeader) {
newBaggageHeaders.push(existingBaggageHeader);
newBaggageHeaders.push(stripBaggageHeaderOfSentryBaggageValues(existingBaggageHeader));
}

if (sentryBaggageHeader) {
Expand Down Expand Up @@ -221,3 +254,13 @@ 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(',')
);
}
Loading