Skip to content

Commit 2eb681e

Browse files
authored
fix(core): Only consider ingest endpoint requests when checking isSentryRequestUrl (#17393)
This PR makes a change to our `isSentryRequestUrl` utility which is used in various parts of the SDKs. The function checks both, the DSN as well as the `tunnel` option to determine if a request URL is a URL to Sentry. I would argue, we should only return `true` for requests to Sentry's ingest endpoint. For example, if users make regular requests to the Sentry API from within their app that uses a Sentry SDK, we should not special case that request. Therefore, this PR makes the check for the request URL more specific: - If `tunnel` is not provided, return `true` iff the the url includes the host of the DSN AND if it includes the `sentry_key` query param. This param is mandatory to be sent along, as it's equal to the [public key of the DSN ](https://develop.sentry.dev/sdk/overview/#parsing-the-dsn). - If `tunnel` is provided, the check was already specific enough because the request URL has to match _exactly_ the configured tunnel URL. While writing this, I realized there are still a bunch of edge cases here that we probably also should fix but for now, let's keep things atomic. closes #17385 (^ very likely. We didn't repro this specifically but the `httpClientIntegration` bails out exactly if it hits the `isSentryRequestUrl` check)
1 parent 444bd82 commit 2eb681e

File tree

5 files changed

+53
-16
lines changed

5 files changed

+53
-16
lines changed

packages/cloudflare/test/integrations/fetch.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ describe('WinterCGFetch instrumentation', () => {
101101
expect(fetchInstrumentationHandlerCallback).toBeDefined();
102102

103103
const startHandlerData: HandlerDataFetch = {
104-
fetchData: { url: 'https://dsn.ingest.sentry.io/1337', method: 'POST' },
105-
args: ['https://dsn.ingest.sentry.io/1337'],
104+
fetchData: { url: 'https://dsn.ingest.sentry.io/1337?sentry_key=123', method: 'POST' },
105+
args: ['https://dsn.ingest.sentry.io/1337?sentry_key=123'],
106106
startTimestamp: Date.now(),
107107
};
108108
fetchInstrumentationHandlerCallback(startHandlerData);

packages/core/src/utils/isSentryRequestUrl.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Client } from '../client';
22
import type { DsnComponents } from '../types-hoist/dsn';
3+
import { isURLObjectRelative, parseStringToURLObject } from './url';
34

45
/**
56
* Checks whether given url points to Sentry server
@@ -21,7 +22,17 @@ function checkTunnel(url: string, tunnel: string | undefined): boolean {
2122
}
2223

2324
function checkDsn(url: string, dsn: DsnComponents | undefined): boolean {
24-
return dsn ? url.includes(dsn.host) : false;
25+
// Requests to Sentry's ingest endpoint must have a `sentry_key` in the query string
26+
// This is equivalent to the public_key which is required in the DSN
27+
// see https://develop.sentry.dev/sdk/overview/#parsing-the-dsn
28+
// Therefore, a request to the same host and with a `sentry_key` in the query string
29+
// can be considered a request to the ingest endpoint.
30+
const urlParts = parseStringToURLObject(url);
31+
if (!urlParts || isURLObjectRelative(urlParts)) {
32+
return false;
33+
}
34+
35+
return dsn ? urlParts.host.includes(dsn.host) && /(^|&|\?)sentry_key=/.test(urlParts.search) : false;
2536
}
2637

2738
function removeTrailingSlash(str: string): string {

packages/core/test/lib/utils/isSentryRequestUrl.test.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,46 @@ import type { Client } from '../../../src/client';
44

55
describe('isSentryRequestUrl', () => {
66
it.each([
7-
['', 'sentry-dsn.com', '', false],
8-
['http://sentry-dsn.com/my-url', 'sentry-dsn.com', '', true],
9-
['http://sentry-dsn.com', 'sentry-dsn.com', '', true],
10-
['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200', true],
11-
['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200/', true],
12-
['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200', true],
13-
['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200', false],
14-
])('works with url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string, expected: boolean) => {
7+
['http://sentry-dsn.com/my-url?sentry_key=123', 'sentry-dsn.com', ''],
8+
9+
['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200'],
10+
['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200/'],
11+
['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200'],
12+
['http://tunnel:4200/', 'another-dsn.com', 'http://tunnel:4200'],
13+
])('returns `true` for url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string) => {
14+
const client = {
15+
getOptions: () => ({ tunnel }),
16+
getDsn: () => ({ host: dsn }),
17+
} as unknown as Client;
18+
19+
expect(isSentryRequestUrl(url, client)).toBe(true);
20+
});
21+
22+
it.each([
23+
['http://tunnel:4200/?sentry_key=123', 'another-dsn.com', ''],
24+
['http://sentry-dsn.com/my-url', 'sentry-dsn.com', ''],
25+
['http://sentry-dsn.com', 'sentry-dsn.com', ''],
26+
['http://sAntry-dsn.com/?sentry_key=123', 'sentry-dsn.com', ''],
27+
['http://sAntry-dsn.com/?sAntry_key=123', 'sAntry-dsn.com', ''],
28+
['/ingest', 'sentry-dsn.com', ''],
29+
['/ingest?sentry_key=123', 'sentry-dsn.com', ''],
30+
['/ingest', '', ''],
31+
['', '', ''],
32+
['', 'sentry-dsn.com', ''],
33+
34+
['http://tunnel:4200/', 'another-dsn.com', 'http://tunnel:4200/sentry-tunnel'],
35+
['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200'],
36+
['http://tunnel:4200/a', '', 'http://tunnel:4200/'],
37+
])('returns `false` for url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string) => {
1538
const client = {
1639
getOptions: () => ({ tunnel }),
1740
getDsn: () => ({ host: dsn }),
1841
} as unknown as Client;
1942

20-
// Works with client passed
21-
expect(isSentryRequestUrl(url, client)).toBe(expected);
43+
expect(isSentryRequestUrl(url, client)).toBe(false);
44+
});
45+
46+
it('handles undefined client', () => {
47+
expect(isSentryRequestUrl('http://sentry-dsn.com/my-url?sentry_key=123', undefined)).toBe(false);
2248
});
2349
});

packages/replay-internal/test/integration/shouldFilterRequest.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ describe('Integration | shouldFilterRequest', () => {
2020
it('should filter requests for Sentry ingest URLs', async () => {
2121
const { replay } = await mockSdk();
2222

23-
expect(shouldFilterRequest(replay, 'https://03031aa.ingest.f00.f00/api/129312/')).toBe(true);
23+
expect(shouldFilterRequest(replay, 'https://03031aa.ingest.f00.f00/api/129312/?sentry_key=123')).toBe(true);
2424
});
2525
});

packages/vercel-edge/test/wintercg-fetch.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ describe('WinterCGFetch instrumentation', () => {
102102
expect(fetchInstrumentationHandlerCallback).toBeDefined();
103103

104104
const startHandlerData: HandlerDataFetch = {
105-
fetchData: { url: 'https://dsn.ingest.sentry.io/1337', method: 'POST' },
106-
args: ['https://dsn.ingest.sentry.io/1337'],
105+
fetchData: { url: 'https://dsn.ingest.sentry.io/1337?sentry_key=123', method: 'POST' },
106+
args: ['https://dsn.ingest.sentry.io/1337?sentry_key=123'],
107107
startTimestamp: Date.now(),
108108
};
109109
fetchInstrumentationHandlerCallback(startHandlerData);

0 commit comments

Comments
 (0)