Skip to content

Commit b420f19

Browse files
author
Luca Forstner
authored
feat: Update default trace propagation targets logic in the browser (#10621)
1 parent be0d7a9 commit b420f19

File tree

14 files changed

+383
-80
lines changed

14 files changed

+383
-80
lines changed

MIGRATION.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# Upgrading from 7.x to 8.x
22

3+
## Updated behaviour of `tracePropagationTargets` in the browser (HTTP tracing headers & CORS)
4+
5+
We updated the behaviour of the SDKs when no `tracePropagationTargets` option was defined. As a reminder, you can
6+
provide a list of strings or RegExes that will be matched against URLs to tell the SDK, to which outgoing requests
7+
tracing HTTP headers should be attached to. These tracing headers are used for distributed tracing.
8+
9+
Previously, on the browser, when `tracePropagationTargets` were not defined, they defaulted to the following:
10+
`['localhost', /^\/(?!\/)/]`. This meant that all request targets to that had "localhost" in the URL, or started with a
11+
`/` were equipped with tracing headers. This default was chosen to prevent CORS errors in your browser applications.
12+
However, this default had a few flaws.
13+
14+
Going forward, when the `tracePropagationTargets` option is not set, tracing headers will be attached to all outgoing
15+
requests on the same origin. For example, if you're on `https://example.com/` and you send a request to
16+
`https://example.com/api`, the request will be traced (ie. will have trace headers attached). Requests to
17+
`https://api.example.com/` will not, because it is on a different origin. The same goes for all applications running on
18+
`localhost`.
19+
20+
When you provide a `tracePropagationTargets` option, all of the entries you defined will now be matched be matched
21+
against the full URL of the outgoing request. Previously, it was only matched against what you called request APIs with.
22+
For example, if you made a request like `fetch("/api/posts")`, the provided `tracePropagationTargets` were only compared
23+
against `"/api/posts"`. Going forward they will be matched against the entire URL, for example, if you were on the page
24+
`https://example.com/` and you made the same request, it would be matched against `"https://example.com/api/posts"`.
25+
26+
But that is not all. Because it would be annoying having to create matchers for the entire URL, if the request is a
27+
same-origin request, we also match the `tracePropagationTargets` against the resolved `pathname` of the request.
28+
Meaning, a matcher like `/^\/api/` would match a request call like `fetch('/api/posts')`, or
29+
`fetch('https://same-origin.com/api/posts')` but not `fetch('https://different-origin.com/api/posts')`.
30+
331
## Removal of the `tracingOrigins` option
432

533
After its deprecation in v7 the `tracingOrigins` option is now removed in favor of the `tracePropagationTargets` option.
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2')));
1+
fetch(`/0`).then(fetch('/1').then(fetch('/2')));

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsMatch/test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@ import { sentryTest } from '../../../../../utils/fixtures';
44
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
55

66
sentryTest(
7-
'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets',
8-
async ({ getLocalTestPath, page }) => {
7+
'should attach `sentry-trace` and `baggage` header to same-origin requests when no tracePropagationTargets are defined',
8+
async ({ getLocalTestUrl, page }) => {
99
if (shouldSkipTracingTest()) {
1010
sentryTest.skip();
1111
}
1212

13-
const url = await getLocalTestPath({ testDir: __dirname });
13+
const url = await getLocalTestUrl({ testDir: __dirname });
1414

1515
const requests = (
16-
await Promise.all([
17-
page.goto(url),
18-
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))),
19-
])
16+
await Promise.all([page.goto(url), Promise.all([0, 1, 2].map(idx => page.waitForRequest(`**/${idx}`)))])
2017
)[1];
2118

2219
expect(requests).toHaveLength(3);

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/defaultTargetsNoMatch/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { sentryTest } from '../../../../../utils/fixtures';
44
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
55

66
sentryTest(
7-
'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets',
7+
'should not attach `sentry-trace` and `baggage` header to cross-origin requests when no tracePropagationTargets are defined',
88
async ({ getLocalTestPath, page }) => {
99
if (shouldSkipTracingTest()) {
1010
sentryTest.skip();
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2')));
1+
fetch('/0').then(fetch('/1').then(fetch('/2')));

dev-packages/browser-integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsMatch/test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@ import { sentryTest } from '../../../../../utils/fixtures';
44
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
55

66
sentryTest(
7-
'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets',
8-
async ({ getLocalTestPath, page }) => {
7+
'should attach `sentry-trace` and `baggage` header to same-origin requests when no tracePropagationTargets are defined',
8+
async ({ getLocalTestUrl, page }) => {
99
if (shouldSkipTracingTest()) {
1010
sentryTest.skip();
1111
}
1212

13-
const url = await getLocalTestPath({ testDir: __dirname });
13+
const url = await getLocalTestUrl({ testDir: __dirname });
1414

1515
const requests = (
16-
await Promise.all([
17-
page.goto(url),
18-
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))),
19-
])
16+
await Promise.all([page.goto(url), Promise.all([0, 1, 2].map(idx => page.waitForRequest(`**/${idx}`)))])
2017
)[1];
2118

2219
expect(requests).toHaveLength(3);
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import * as Sentry from '@sentry/browser';
2-
import { Integrations } from '@sentry/tracing';
32

43
window.Sentry = Sentry;
54

65
Sentry.init({
76
dsn: 'https://[email protected]/1337',
8-
integrations: [new Integrations.BrowserTracing()],
7+
integrations: [Sentry.browserTracingIntegration()],
98
tracesSampleRate: 1,
109
});

dev-packages/browser-integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsNoMatch/test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@ import { sentryTest } from '../../../../../utils/fixtures';
44
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
55

66
sentryTest(
7-
'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets',
8-
async ({ getLocalTestPath, page }) => {
7+
'should not attach `sentry-trace` and `baggage` header to cross-origin requests when no tracePropagationTargets are defined',
8+
async ({ getLocalTestUrl, page }) => {
99
if (shouldSkipTracingTest()) {
1010
sentryTest.skip();
1111
}
1212

13-
const url = await getLocalTestPath({ testDir: __dirname });
13+
const url = await getLocalTestUrl({ testDir: __dirname });
1414

1515
const requests = (
16-
await Promise.all([
17-
page.goto(url),
18-
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
19-
])
16+
await Promise.all([page.goto(url), Promise.all([0, 1, 2].map(idx => page.waitForRequest(`**/${idx}`)))])
2017
)[1];
2118

2219
expect(requests).toHaveLength(3);

packages/browser/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ export {
6262
browserTracingIntegration,
6363
startBrowserTracingNavigationSpan,
6464
startBrowserTracingPageLoadSpan,
65-
DEFAULT_TRACE_PROPAGATION_TARGETS,
6665
} from '@sentry-internal/tracing';
6766
export type { RequestInstrumentationOptions } from '@sentry-internal/tracing';
6867
export {

packages/nextjs/src/client/index.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { addEventProcessor, applySdkMetadata, hasTracingEnabled, setTag } from '@sentry/core';
22
import type { BrowserOptions } from '@sentry/react';
3-
import {
4-
DEFAULT_TRACE_PROPAGATION_TARGETS,
5-
getDefaultIntegrations as getReactDefaultIntegrations,
6-
init as reactInit,
7-
} from '@sentry/react';
3+
import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react';
84
import type { EventProcessor, Integration } from '@sentry/types';
95

106
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
@@ -28,17 +24,6 @@ declare const __SENTRY_TRACING__: boolean;
2824
export function init(options: BrowserOptions): void {
2925
const opts = {
3026
environment: getVercelEnv(true) || process.env.NODE_ENV,
31-
32-
tracePropagationTargets:
33-
process.env.NODE_ENV === 'development'
34-
? [
35-
// Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server
36-
// has cors and it doesn't like extra headers when it's accessed from a different URL.
37-
// TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764)
38-
/^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/,
39-
/^\/(?!\/)/,
40-
]
41-
: [...DEFAULT_TRACE_PROPAGATION_TARGETS, /^(api\/)/],
4227
defaultIntegrations: getDefaultIntegrations(options),
4328
...options,
4429
} satisfies BrowserOptions;

packages/tracing-internal/src/browser/request.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable max-lines */
21
import {
32
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
43
getClient,
@@ -26,18 +25,38 @@ import {
2625

2726
import { instrumentFetchRequest } from '../common/fetch';
2827
import { addPerformanceInstrumentationHandler } from './instrument';
29-
30-
export const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
28+
import { WINDOW } from './types';
3129

3230
/** Options for Request Instrumentation */
3331
export interface RequestInstrumentationOptions {
3432
/**
3533
* List of strings and/or Regular Expressions used to determine which outgoing requests will have `sentry-trace` and `baggage`
3634
* headers attached.
3735
*
38-
* Default: ['localhost', /^\//]
36+
* **Default:** If this option is not provided, tracing headers will be attached to all outgoing requests.
37+
* If you are using a browser SDK, by default, tracing headers will only be attached to outgoing requests to the same origin.
38+
*
39+
* **Disclaimer:** Carelessly setting this option in browser environments may result into CORS errors!
40+
* Only attach tracing headers to requests to the same origin, or to requests to services you can control CORS headers of.
41+
* Cross-origin requests, meaning requests to a different domain, for example a request to `https://api.example.com/` while you're on `https://example.com/`, take special care.
42+
* If you are attaching headers to cross-origin requests, make sure the backend handling the request returns a `"Access-Control-Allow-Headers: sentry-trace, baggage"` header to ensure your requests aren't blocked.
43+
*
44+
* If you provide a `tracePropagationTargets` array, the entries you provide will be matched against the entire URL of the outgoing request.
45+
* If you are using a browser SDK, the entries will also be matched against the pathname of the outgoing requests.
46+
* This is so you can have matchers for relative requests, for example, `/^\/api/` if you want to trace requests to your `/api` routes on the same domain.
47+
*
48+
* If any of the two match any of the provided values, tracing headers will be attached to the outgoing request.
49+
* Both, the string values, and the RegExes you provide in the array will match if they partially match the URL or pathname.
50+
*
51+
* Examples:
52+
* - `tracePropagationTargets: [/^\/api/]` and request to `https://same-origin.com/api/posts`:
53+
* - Tracing headers will be attached because the request is sent to the same origin and the regex matches the pathname "/api/posts".
54+
* - `tracePropagationTargets: [/^\/api/]` and request to `https://different-origin.com/api/posts`:
55+
* - Tracing headers will not be attached because the pathname will only be compared when the request target lives on the same origin.
56+
* - `tracePropagationTargets: [/^\/api/, 'https://external-api.com']` and request to `https://external-api.com/v1/data`:
57+
* - Tracing headers will be attached because the request URL matches the string `'https://external-api.com'`.
3958
*/
40-
tracePropagationTargets: Array<string | RegExp>;
59+
tracePropagationTargets?: Array<string | RegExp>;
4160

4261
/**
4362
* Flag to disable patching all together for fetch requests.
@@ -73,7 +92,6 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions
7392
traceFetch: true,
7493
traceXHR: true,
7594
enableHTTPTimings: true,
76-
tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
7795
};
7896

7997
/** Registers span creators for xhr and fetch requests */
@@ -207,19 +225,55 @@ function resourceTimingEntryToSpanData(resourceTiming: PerformanceResourceTiming
207225

208226
/**
209227
* A function that determines whether to attach tracing headers to a request.
210-
* This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
211-
* We only export this fuction for testing purposes.
228+
* We only export this function for testing purposes.
212229
*/
213-
export function shouldAttachHeaders(url: string, tracePropagationTargets: (string | RegExp)[] | undefined): boolean {
214-
return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
230+
export function shouldAttachHeaders(
231+
targetUrl: string,
232+
tracePropagationTargets: (string | RegExp)[] | undefined,
233+
): boolean {
234+
// window.location.href not being defined is an edge case in the browser but we need to handle it.
235+
// Potentially dangerous situations where it may not be defined: Browser Extensions, Web Workers, patching of the location obj
236+
const href: string | undefined = WINDOW.location && WINDOW.location.href;
237+
238+
if (!href) {
239+
// If there is no window.location.origin, we default to only attaching tracing headers to relative requests, i.e. ones that start with `/`
240+
// BIG DISCLAIMER: Users can call URLs with a double slash (fetch("//example.com/api")), this is a shorthand for "send to the same protocol",
241+
// so we need a to exclude those requests, because they might be cross origin.
242+
const isRelativeSameOriginRequest = !!targetUrl.match(/^\/(?!\/)/);
243+
if (!tracePropagationTargets) {
244+
return isRelativeSameOriginRequest;
245+
} else {
246+
return stringMatchesSomePattern(targetUrl, tracePropagationTargets);
247+
}
248+
} else {
249+
let resolvedUrl;
250+
let currentOrigin;
251+
252+
// URL parsing may fail, we default to not attaching trace headers in that case.
253+
try {
254+
resolvedUrl = new URL(targetUrl, href);
255+
currentOrigin = new URL(href).origin;
256+
} catch (e) {
257+
return false;
258+
}
259+
260+
const isSameOriginRequest = resolvedUrl.origin === currentOrigin;
261+
if (!tracePropagationTargets) {
262+
return isSameOriginRequest;
263+
} else {
264+
return (
265+
stringMatchesSomePattern(resolvedUrl.toString(), tracePropagationTargets) ||
266+
(isSameOriginRequest && stringMatchesSomePattern(resolvedUrl.pathname, tracePropagationTargets))
267+
);
268+
}
269+
}
215270
}
216271

217272
/**
218273
* Create and track xhr request spans
219274
*
220275
* @returns Span if a span was created, otherwise void.
221276
*/
222-
// eslint-disable-next-line complexity
223277
export function xhrCallback(
224278
handlerData: HandlerDataXhr,
225279
shouldCreateSpan: (url: string) => boolean,

packages/tracing-internal/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,3 @@ export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './commo
3232
export type { RequestInstrumentationOptions } from './browser';
3333

3434
export { addExtensionMethods } from './extensions';
35-
36-
export { DEFAULT_TRACE_PROPAGATION_TARGETS } from './browser/request';

0 commit comments

Comments
 (0)