Skip to content

Commit 090d08c

Browse files
JPeer264Lms24
andauthored
feat(core): Add option to enhance the fetch error message (#18466)
(closes #18449) (closes [JS-1281](https://linear.app/getsentry/issue/JS-1281/appending-hostname-to-fetch-error-messages-breaks-is-network-error-and)) ## Problem As of now, the user has no chance to disallow the manipulation of fetch errors, as we overwrite the error. This can cause problems as seen in #18449. ## Solution This adds a new option for the SDK (please be very critical about that new option here, since `fetch` has no integration this has to be added as a `init` option). `always` is the default and acts the same as it is now, so it is acting as feature: ```ts enhanceFetchErrorMessages: 'always' | 'report-only' | false` ``` To give the user full control of how the errors are done there are 3 settings: | | always | report-only | false | | ------------------------------------------ | ------- | ----------- | - | manipulate the error message directly | ✅ | ❌ | ❌ | send only the changed message to Sentry | ✅ | ✅ | ❌ ## Special attention to reviewers When having `report-only` the generated logs locally differ from the ones in Sentry. I am not quite sure if that would cause any problems. This is the only question which I don't have the answer to yet ## Alternative In case the size increase is too much, we can also have a boolean that disables that (which is on by default) --------- Co-authored-by: Lukas Stracke <[email protected]>
1 parent 82b9756 commit 090d08c

File tree

15 files changed

+625
-27
lines changed

15 files changed

+625
-27
lines changed

.size-limit.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module.exports = [
88
path: 'packages/browser/build/npm/esm/prod/index.js',
99
import: createImport('init'),
1010
gzip: true,
11-
limit: '25 KB',
11+
limit: '25.5 KB',
1212
},
1313
{
1414
name: '@sentry/browser - with treeshaking flags',
@@ -148,7 +148,7 @@ module.exports = [
148148
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
149149
ignore: ['react/jsx-runtime'],
150150
gzip: true,
151-
limit: '44 KB',
151+
limit: '44.5 KB',
152152
},
153153
// Vue SDK (ESM)
154154
{
@@ -171,20 +171,20 @@ module.exports = [
171171
path: 'packages/svelte/build/esm/index.js',
172172
import: createImport('init'),
173173
gzip: true,
174-
limit: '25 KB',
174+
limit: '25.5 KB',
175175
},
176176
// Browser CDN bundles
177177
{
178178
name: 'CDN Bundle',
179179
path: createCDNPath('bundle.min.js'),
180180
gzip: true,
181-
limit: '27.5 KB',
181+
limit: '28 KB',
182182
},
183183
{
184184
name: 'CDN Bundle (incl. Tracing)',
185185
path: createCDNPath('bundle.tracing.min.js'),
186186
gzip: true,
187-
limit: '42.5 KB',
187+
limit: '43 KB',
188188
},
189189
{
190190
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -234,7 +234,7 @@ module.exports = [
234234
import: createImport('init'),
235235
ignore: ['next/router', 'next/constants'],
236236
gzip: true,
237-
limit: '46.5 KB',
237+
limit: '47 KB',
238238
},
239239
// SvelteKit SDK (ESM)
240240
{
@@ -243,7 +243,7 @@ module.exports = [
243243
import: createImport('init'),
244244
ignore: ['$app/stores'],
245245
gzip: true,
246-
limit: '42.5 KB',
246+
limit: '43 KB',
247247
},
248248
// Node-Core SDK (ESM)
249249
{
@@ -261,7 +261,7 @@ module.exports = [
261261
import: createImport('init'),
262262
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
263263
gzip: true,
264-
limit: '162.5 KB',
264+
limit: '163 KB',
265265
},
266266
{
267267
name: '@sentry/node - without tracing',
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
enhanceFetchErrorMessages: false,
8+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Based on possible TypeError exceptions from https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
2+
3+
// Network error (e.g. ad-blocked, offline, page does not exist, ...)
4+
window.networkError = () => {
5+
fetch('http://sentry-test-external.io/does-not-exist');
6+
};
7+
8+
window.networkErrorSubdomain = () => {
9+
fetch('http://subdomain.sentry-test-external.io/does-not-exist');
10+
};
11+
12+
window.networkErrorWithPort = () => {
13+
fetch('http://sentry-test-external.io:3000/does-not-exist');
14+
};
15+
16+
// Invalid header also produces TypeError
17+
window.invalidHeaderName = () => {
18+
fetch('http://sentry-test-external.io/invalid-header-name', { headers: { 'C ontent-Type': 'text/xml' } });
19+
};
20+
21+
// Invalid header value also produces TypeError
22+
window.invalidHeaderValue = () => {
23+
fetch('http://sentry-test-external.io/invalid-header-value', { headers: ['Content-Type', 'text/html', 'extra'] });
24+
};
25+
26+
// Invalid URL scheme
27+
window.invalidUrlScheme = () => {
28+
fetch('blub://sentry-test-external.io/invalid-scheme');
29+
};
30+
31+
// URL includes credentials
32+
window.credentialsInUrl = () => {
33+
fetch('https://user:[email protected]/credentials-in-url');
34+
};
35+
36+
// Invalid mode
37+
window.invalidMode = () => {
38+
fetch('https://sentry-test-external.io/invalid-mode', { mode: 'navigate' });
39+
};
40+
41+
// Invalid request method
42+
window.invalidMethod = () => {
43+
fetch('http://sentry-test-external.io/invalid-method', { method: 'CONNECT' });
44+
};
45+
46+
// No-cors mode with cors-required method
47+
window.noCorsMethod = () => {
48+
fetch('http://sentry-test-external.io/no-cors-method', { mode: 'no-cors', method: 'PUT' });
49+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
4+
5+
sentryTest(
6+
'enhanceFetchErrorMessages: false: enhances error for Sentry while preserving original',
7+
async ({ getLocalTestUrl, page, browserName }) => {
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
const reqPromise = waitForErrorRequest(page);
10+
const pageErrorPromise = new Promise<string>(resolve => {
11+
page.on('pageerror', error => {
12+
resolve(error.message);
13+
});
14+
});
15+
16+
await page.goto(url);
17+
await page.evaluate('networkError()');
18+
19+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
20+
const eventData = envelopeRequestParser(req);
21+
const originalErrorMap: Record<string, string> = {
22+
chromium: 'Failed to fetch',
23+
webkit: 'Load failed',
24+
firefox: 'NetworkError when attempting to fetch resource.',
25+
};
26+
27+
const originalError = originalErrorMap[browserName];
28+
29+
expect(pageErrorMessage).toContain(originalError);
30+
expect(pageErrorMessage).not.toContain('sentry-test-external.io');
31+
32+
expect(eventData.exception?.values).toHaveLength(1);
33+
expect(eventData.exception?.values?.[0]).toMatchObject({
34+
type: 'TypeError',
35+
value: originalError,
36+
mechanism: {
37+
handled: false,
38+
type: 'auto.browser.global_handlers.onunhandledrejection',
39+
},
40+
});
41+
},
42+
);
43+
44+
sentryTest(
45+
'enhanceFetchErrorMessages: false: enhances subdomain errors',
46+
async ({ getLocalTestUrl, page, browserName }) => {
47+
const url = await getLocalTestUrl({ testDir: __dirname });
48+
const reqPromise = waitForErrorRequest(page);
49+
const pageErrorPromise = new Promise<string>(resolve => page.on('pageerror', error => resolve(error.message)));
50+
51+
await page.goto(url);
52+
await page.evaluate('networkErrorSubdomain()');
53+
54+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
55+
const eventData = envelopeRequestParser(req);
56+
57+
const originalErrorMap: Record<string, string> = {
58+
chromium: 'Failed to fetch',
59+
webkit: 'Load failed',
60+
firefox: 'NetworkError when attempting to fetch resource.',
61+
};
62+
63+
const originalError = originalErrorMap[browserName];
64+
65+
expect(pageErrorMessage).toContain(originalError);
66+
expect(pageErrorMessage).not.toContain('subdomain.sentry-test-external.io');
67+
expect(eventData.exception?.values).toHaveLength(1);
68+
expect(eventData.exception?.values?.[0]).toMatchObject({
69+
type: 'TypeError',
70+
value: originalError,
71+
mechanism: {
72+
handled: false,
73+
type: 'auto.browser.global_handlers.onunhandledrejection',
74+
},
75+
});
76+
},
77+
);
78+
79+
sentryTest(
80+
'enhanceFetchErrorMessages: false: includes port in hostname',
81+
async ({ getLocalTestUrl, page, browserName }) => {
82+
const url = await getLocalTestUrl({ testDir: __dirname });
83+
const reqPromise = waitForErrorRequest(page);
84+
85+
const pageErrorPromise = new Promise<string>(resolve => page.on('pageerror', error => resolve(error.message)));
86+
87+
await page.goto(url);
88+
await page.evaluate('networkErrorWithPort()');
89+
90+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
91+
const eventData = envelopeRequestParser(req);
92+
93+
const originalErrorMap: Record<string, string> = {
94+
chromium: 'Failed to fetch',
95+
webkit: 'Load failed',
96+
firefox: 'NetworkError when attempting to fetch resource.',
97+
};
98+
99+
const originalError = originalErrorMap[browserName];
100+
101+
expect(pageErrorMessage).toContain(originalError);
102+
expect(pageErrorMessage).not.toContain('sentry-test-external.io:3000');
103+
expect(eventData.exception?.values).toHaveLength(1);
104+
expect(eventData.exception?.values?.[0]).toMatchObject({
105+
type: 'TypeError',
106+
value: originalError,
107+
mechanism: {
108+
handled: false,
109+
type: 'auto.browser.global_handlers.onunhandledrejection',
110+
},
111+
});
112+
},
113+
);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
enhanceFetchErrorMessages: 'report-only',
8+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Based on possible TypeError exceptions from https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
2+
3+
// Network error (e.g. ad-blocked, offline, page does not exist, ...)
4+
window.networkError = () => {
5+
fetch('http://sentry-test-external.io/does-not-exist');
6+
};
7+
8+
window.networkErrorSubdomain = () => {
9+
fetch('http://subdomain.sentry-test-external.io/does-not-exist');
10+
};
11+
12+
window.networkErrorWithPort = () => {
13+
fetch('http://sentry-test-external.io:3000/does-not-exist');
14+
};
15+
16+
// Invalid header also produces TypeError
17+
window.invalidHeaderName = () => {
18+
fetch('http://sentry-test-external.io/invalid-header-name', { headers: { 'C ontent-Type': 'text/xml' } });
19+
};
20+
21+
// Invalid header value also produces TypeError
22+
window.invalidHeaderValue = () => {
23+
fetch('http://sentry-test-external.io/invalid-header-value', { headers: ['Content-Type', 'text/html', 'extra'] });
24+
};
25+
26+
// Invalid URL scheme
27+
window.invalidUrlScheme = () => {
28+
fetch('blub://sentry-test-external.io/invalid-scheme');
29+
};
30+
31+
// URL includes credentials
32+
window.credentialsInUrl = () => {
33+
fetch('https://user:[email protected]/credentials-in-url');
34+
};
35+
36+
// Invalid mode
37+
window.invalidMode = () => {
38+
fetch('https://sentry-test-external.io/invalid-mode', { mode: 'navigate' });
39+
};
40+
41+
// Invalid request method
42+
window.invalidMethod = () => {
43+
fetch('http://sentry-test-external.io/invalid-method', { method: 'CONNECT' });
44+
};
45+
46+
// No-cors mode with cors-required method
47+
window.noCorsMethod = () => {
48+
fetch('http://sentry-test-external.io/no-cors-method', { mode: 'no-cors', method: 'PUT' });
49+
};

0 commit comments

Comments
 (0)