Skip to content

Commit 6e6f85b

Browse files
authored
fix(core): Ensure http.client span descriptions don't contain query params or fragments (#15404)
This patch fixes an oversight with our `fetch` instrumentation in the core package and the browser XHR instrumentation. We didn't strip query params and URL hash fragments from the span name (description) of `http.client` spans. With this fix, the span description now only contains the URL protocol, host and path as defined in our [develop specification](https://develop.sentry.dev/sdk/expected-features/data-handling/#spans).
1 parent 5e6b852 commit 6e6f85b

File tree

21 files changed

+821
-23
lines changed

21 files changed

+821
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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({ instrumentPageLoad: false, instrumentNavigation: false })],
8+
tracePropagationTargets: ['http://sentry-test-site.example'],
9+
tracesSampleRate: 1,
10+
autoSessionTracking: false,
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
function withRootSpan(cb) {
2+
return Sentry.startSpan({ name: 'rootSpan' }, cb);
3+
}
4+
5+
document.getElementById('btnQuery').addEventListener('click', async () => {
6+
await withRootSpan(() => fetch('http://sentry-test-site.example/0?id=123;page=5'));
7+
});
8+
9+
document.getElementById('btnFragment').addEventListener('click', async () => {
10+
await withRootSpan(() => fetch('http://sentry-test-site.example/1#fragment'));
11+
});
12+
13+
document.getElementById('btnQueryFragment').addEventListener('click', async () => {
14+
await withRootSpan(() => fetch('http://sentry-test-site.example/2?id=1#fragment'));
15+
});
16+
17+
document.getElementById('btnQueryFragmentSameOrigin').addEventListener('click', async () => {
18+
await withRootSpan(() => fetch('/api/users?id=1#fragment'));
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="btnQuery">Request with query</button>
8+
<button id="btnFragment">Request with fragment</button>
9+
<button id="btnQueryFragment">Request with query and fragment</button>
10+
<button id="btnQueryFragmentSameOrigin">Request with query and fragment</button>
11+
</body>
12+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
4+
5+
sentryTest('strips query params in fetch request spans', async ({ getLocalTestUrl, page }) => {
6+
if (shouldSkipTracingTest()) {
7+
sentryTest.skip();
8+
}
9+
10+
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
await page.goto(url);
15+
16+
const txnPromise = waitForTransactionRequest(page);
17+
await page.locator('#btnQuery').click();
18+
const transactionEvent = envelopeRequestParser(await txnPromise);
19+
20+
expect(transactionEvent.transaction).toEqual('rootSpan');
21+
22+
const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client');
23+
24+
expect(requestSpan).toMatchObject({
25+
description: 'GET http://sentry-test-site.example/0',
26+
parent_span_id: transactionEvent.contexts?.trace?.span_id,
27+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
28+
start_timestamp: expect.any(Number),
29+
timestamp: expect.any(Number),
30+
trace_id: transactionEvent.contexts?.trace?.trace_id,
31+
data: expect.objectContaining({
32+
'http.method': 'GET',
33+
'http.url': 'http://sentry-test-site.example/0?id=123;page=5',
34+
'http.query': '?id=123;page=5',
35+
'http.response.status_code': 200,
36+
'http.response_content_length': 2,
37+
'sentry.op': 'http.client',
38+
'sentry.origin': 'auto.http.browser',
39+
type: 'fetch',
40+
'server.address': 'sentry-test-site.example',
41+
url: 'http://sentry-test-site.example/0?id=123;page=5',
42+
}),
43+
});
44+
45+
expect(requestSpan?.data).not.toHaveProperty('http.fragment');
46+
});
47+
48+
sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestUrl, page }) => {
49+
if (shouldSkipTracingTest()) {
50+
sentryTest.skip();
51+
}
52+
53+
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
54+
55+
const url = await getLocalTestUrl({ testDir: __dirname });
56+
57+
await page.goto(url);
58+
59+
const txnPromise = waitForTransactionRequest(page);
60+
await page.locator('#btnFragment').click();
61+
const transactionEvent = envelopeRequestParser(await txnPromise);
62+
63+
expect(transactionEvent.transaction).toEqual('rootSpan');
64+
65+
const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client');
66+
67+
expect(requestSpan).toMatchObject({
68+
description: 'GET http://sentry-test-site.example/1',
69+
parent_span_id: transactionEvent.contexts?.trace?.span_id,
70+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
71+
start_timestamp: expect.any(Number),
72+
timestamp: expect.any(Number),
73+
trace_id: transactionEvent.contexts?.trace?.trace_id,
74+
data: expect.objectContaining({
75+
'http.method': 'GET',
76+
'http.url': 'http://sentry-test-site.example/1#fragment',
77+
'http.fragment': '#fragment',
78+
'http.response.status_code': 200,
79+
'http.response_content_length': 2,
80+
'sentry.op': 'http.client',
81+
'sentry.origin': 'auto.http.browser',
82+
type: 'fetch',
83+
'server.address': 'sentry-test-site.example',
84+
url: 'http://sentry-test-site.example/1#fragment',
85+
}),
86+
});
87+
88+
expect(requestSpan?.data).not.toHaveProperty('http.query');
89+
});
90+
91+
sentryTest('strips hash fragment and query params in fetch request spans', async ({ getLocalTestUrl, page }) => {
92+
if (shouldSkipTracingTest()) {
93+
sentryTest.skip();
94+
}
95+
96+
await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' }));
97+
98+
const url = await getLocalTestUrl({ testDir: __dirname });
99+
100+
await page.goto(url);
101+
102+
const txnPromise = waitForTransactionRequest(page);
103+
await page.locator('#btnQueryFragment').click();
104+
const transactionEvent = envelopeRequestParser(await txnPromise);
105+
106+
expect(transactionEvent.transaction).toEqual('rootSpan');
107+
108+
const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client');
109+
110+
expect(requestSpan).toMatchObject({
111+
description: 'GET http://sentry-test-site.example/2',
112+
parent_span_id: transactionEvent.contexts?.trace?.span_id,
113+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
114+
start_timestamp: expect.any(Number),
115+
timestamp: expect.any(Number),
116+
trace_id: transactionEvent.contexts?.trace?.trace_id,
117+
data: expect.objectContaining({
118+
'http.method': 'GET',
119+
'http.url': 'http://sentry-test-site.example/2?id=1#fragment',
120+
'http.query': '?id=1',
121+
'http.fragment': '#fragment',
122+
'http.response.status_code': 200,
123+
'http.response_content_length': 2,
124+
'sentry.op': 'http.client',
125+
'sentry.origin': 'auto.http.browser',
126+
type: 'fetch',
127+
'server.address': 'sentry-test-site.example',
128+
url: 'http://sentry-test-site.example/2?id=1#fragment',
129+
}),
130+
});
131+
});
132+
133+
sentryTest(
134+
'strips hash fragment and query params in same-origin fetch request spans',
135+
async ({ getLocalTestUrl, page }) => {
136+
if (shouldSkipTracingTest()) {
137+
sentryTest.skip();
138+
}
139+
140+
await page.route('**/*', route => route.fulfill({ body: 'ok' }));
141+
142+
const url = await getLocalTestUrl({ testDir: __dirname });
143+
144+
await page.goto(url);
145+
146+
const txnPromise = waitForTransactionRequest(page);
147+
await page.locator('#btnQueryFragmentSameOrigin').click();
148+
const transactionEvent = envelopeRequestParser(await txnPromise);
149+
150+
expect(transactionEvent.transaction).toEqual('rootSpan');
151+
152+
const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client');
153+
154+
expect(requestSpan).toMatchObject({
155+
description: 'GET /api/users',
156+
parent_span_id: transactionEvent.contexts?.trace?.span_id,
157+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
158+
start_timestamp: expect.any(Number),
159+
timestamp: expect.any(Number),
160+
trace_id: transactionEvent.contexts?.trace?.trace_id,
161+
data: expect.objectContaining({
162+
'http.method': 'GET',
163+
'http.url': 'http://sentry-test.io/api/users?id=1#fragment',
164+
'http.query': '?id=1',
165+
'http.fragment': '#fragment',
166+
'http.response.status_code': 200,
167+
'http.response_content_length': 2,
168+
'sentry.op': 'http.client',
169+
'sentry.origin': 'auto.http.browser',
170+
type: 'fetch',
171+
'server.address': 'sentry-test.io',
172+
url: '/api/users?id=1#fragment',
173+
}),
174+
});
175+
},
176+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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({ instrumentPageLoad: false, instrumentNavigation: false })],
8+
tracePropagationTargets: ['http://sentry-test-site.example'],
9+
tracesSampleRate: 1,
10+
autoSessionTracking: false,
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
function withRootSpan(cb) {
2+
return Sentry.startSpan({ name: 'rootSpan' }, cb);
3+
}
4+
5+
function makeXHRRequest(url) {
6+
return new Promise((resolve, reject) => {
7+
const xhr = new XMLHttpRequest();
8+
xhr.open('GET', url);
9+
xhr.onload = () => resolve(xhr.responseText);
10+
xhr.onerror = () => reject(xhr.statusText);
11+
xhr.send();
12+
});
13+
}
14+
15+
document.getElementById('btnQuery').addEventListener('click', async () => {
16+
await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/0?id=123;page=5'));
17+
});
18+
19+
document.getElementById('btnFragment').addEventListener('click', async () => {
20+
await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/1#fragment'));
21+
});
22+
23+
document.getElementById('btnQueryFragment').addEventListener('click', async () => {
24+
await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/2?id=1#fragment'));
25+
});
26+
27+
document.getElementById('btnQueryFragmentSameOrigin').addEventListener('click', async () => {
28+
await withRootSpan(() => makeXHRRequest('/api/users?id=1#fragment'));
29+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="btnQuery">Request with query</button>
8+
<button id="btnFragment">Request with fragment</button>
9+
<button id="btnQueryFragment">Request with query and fragment</button>
10+
<button id="btnQueryFragmentSameOrigin">Same origin request with query and fragment</button>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)