Skip to content

Commit 47d33f2

Browse files
committed
feat(core): Add option to enhance the fetch error message
1 parent 8596086 commit 47d33f2

File tree

14 files changed

+597
-10
lines changed

14 files changed

+597
-10
lines changed
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://public@dsn.ingest.sentry.io/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:password@sentry-test-external.io/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 @firefox',
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 @firefox',
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 @firefox',
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://public@dsn.ingest.sentry.io/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:password@sentry-test-external.io/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: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
4+
5+
sentryTest(
6+
'enhanceFetchErrorMessages: report-only: enhances error for Sentry while preserving original @firefox',
7+
async ({ getLocalTestUrl, page, browserName }) => {
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
const reqPromise = waitForErrorRequest(page);
10+
const pageErrorPromise = new Promise<string>(resolve => page.on('pageerror', error => resolve(error.message)));
11+
12+
await page.goto(url);
13+
await page.evaluate('networkError()');
14+
15+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
16+
const eventData = envelopeRequestParser(req);
17+
const originalErrorMap: Record<string, string> = {
18+
chromium: 'Failed to fetch',
19+
webkit: 'Load failed',
20+
firefox: 'NetworkError when attempting to fetch resource.',
21+
};
22+
23+
const enhancedErrorMap: Record<string, string> = {
24+
chromium: 'Failed to fetch (sentry-test-external.io)',
25+
webkit: 'Load failed (sentry-test-external.io)',
26+
firefox: 'NetworkError when attempting to fetch resource. (sentry-test-external.io)',
27+
};
28+
29+
const originalError = originalErrorMap[browserName];
30+
const enhancedError = enhancedErrorMap[browserName];
31+
32+
expect(pageErrorMessage).toContain(originalError);
33+
expect(pageErrorMessage).not.toContain('sentry-test-external.io');
34+
35+
// Verify Sentry received the enhanced message
36+
// Note: In report-only mode, the original error message remains unchanged
37+
// at the JavaScript level (for third-party package compatibility),
38+
// but Sentry gets the enhanced version via __sentry_fetch_url_host__
39+
expect(eventData.exception?.values).toHaveLength(1);
40+
expect(eventData.exception?.values?.[0]).toMatchObject({
41+
type: 'TypeError',
42+
value: enhancedError,
43+
mechanism: {
44+
handled: false,
45+
type: 'auto.browser.global_handlers.onunhandledrejection',
46+
},
47+
});
48+
},
49+
);
50+
51+
sentryTest(
52+
'enhanceFetchErrorMessages: report-only: enhances subdomain errors @firefox',
53+
async ({ getLocalTestUrl, page, browserName }) => {
54+
const url = await getLocalTestUrl({ testDir: __dirname });
55+
const reqPromise = waitForErrorRequest(page);
56+
const pageErrorPromise = new Promise<string>(resolve => page.on('pageerror', error => resolve(error.message)));
57+
58+
await page.goto(url);
59+
await page.evaluate('networkErrorSubdomain()');
60+
61+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
62+
const eventData = envelopeRequestParser(req);
63+
64+
const originalErrorMap: Record<string, string> = {
65+
chromium: 'Failed to fetch',
66+
webkit: 'Load failed',
67+
firefox: 'NetworkError when attempting to fetch resource.',
68+
};
69+
70+
const enhancedErrorMap: Record<string, string> = {
71+
chromium: 'Failed to fetch (subdomain.sentry-test-external.io)',
72+
webkit: 'Load failed (subdomain.sentry-test-external.io)',
73+
firefox: 'NetworkError when attempting to fetch resource. (subdomain.sentry-test-external.io)',
74+
};
75+
76+
const originalError = originalErrorMap[browserName];
77+
const enhancedError = enhancedErrorMap[browserName];
78+
79+
expect(pageErrorMessage).toContain(originalError);
80+
expect(pageErrorMessage).not.toContain('subdomain.sentry-test-external.io');
81+
expect(eventData.exception?.values).toHaveLength(1);
82+
expect(eventData.exception?.values?.[0]).toMatchObject({
83+
type: 'TypeError',
84+
value: enhancedError,
85+
mechanism: {
86+
handled: false,
87+
type: 'auto.browser.global_handlers.onunhandledrejection',
88+
},
89+
});
90+
},
91+
);
92+
93+
sentryTest(
94+
'enhanceFetchErrorMessages: report-only: includes port in hostname @firefox',
95+
async ({ getLocalTestUrl, page, browserName }) => {
96+
const url = await getLocalTestUrl({ testDir: __dirname });
97+
const reqPromise = waitForErrorRequest(page);
98+
99+
const pageErrorPromise = new Promise<string>(resolve => page.on('pageerror', error => resolve(error.message)));
100+
101+
await page.goto(url);
102+
await page.evaluate('networkErrorWithPort()');
103+
104+
const [req, pageErrorMessage] = await Promise.all([reqPromise, pageErrorPromise]);
105+
const eventData = envelopeRequestParser(req);
106+
107+
const originalErrorMap: Record<string, string> = {
108+
chromium: 'Failed to fetch',
109+
webkit: 'Load failed',
110+
firefox: 'NetworkError when attempting to fetch resource.',
111+
};
112+
113+
const enhancedErrorMap: Record<string, string> = {
114+
chromium: 'Failed to fetch (sentry-test-external.io:3000)',
115+
webkit: 'Load failed (sentry-test-external.io:3000)',
116+
firefox: 'NetworkError when attempting to fetch resource. (sentry-test-external.io:3000)',
117+
};
118+
119+
const originalError = originalErrorMap[browserName];
120+
const enhancedError = enhancedErrorMap[browserName];
121+
122+
expect(pageErrorMessage).toContain(originalError);
123+
expect(pageErrorMessage).not.toContain('sentry-test-external.io:3000');
124+
expect(eventData.exception?.values).toHaveLength(1);
125+
expect(eventData.exception?.values?.[0]).toMatchObject({
126+
type: 'TypeError',
127+
value: enhancedError,
128+
mechanism: {
129+
handled: false,
130+
type: 'auto.browser.global_handlers.onunhandledrejection',
131+
},
132+
});
133+
},
134+
);

0 commit comments

Comments
 (0)