Skip to content

Commit c4f3edf

Browse files
committed
feat(browser): Add outgoingRequest context to fetch errors
For now, this only adds `method` and `url` fields.
1 parent ef2f35d commit c4f3edf

File tree

4 files changed

+71
-1
lines changed

4 files changed

+71
-1
lines changed

dev-packages/browser-integration-tests/suites/errors/fetch/subject.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ window.credentialsInUrl = () => {
3131

3232
// Invalid mode
3333
window.invalidMode = () => {
34-
fetch('https://sentry-test-external.io/invalid-mode', { mode: 'navigate' });
34+
fetch('http://sentry-test-external.io/invalid-mode', { mode: 'navigate' });
3535
};
3636

3737
// Invalid request method

dev-packages/browser-integration-tests/suites/errors/fetch/test.ts

+36
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ sentryTest('handles fetch network errors @firefox', async ({ getLocalTestUrl, pa
2727
type: 'onunhandledrejection',
2828
},
2929
});
30+
expect(eventData.contexts?.outgoingRequest).toEqual({
31+
method: 'GET',
32+
url: 'http://sentry-test-external.io/does-not-exist',
33+
});
3034
});
3135

3236

@@ -55,6 +59,10 @@ sentryTest('handles fetch network errors on subdomains @firefox', async ({ getLo
5559
type: 'onunhandledrejection',
5660
},
5761
});
62+
expect(eventData.contexts?.outgoingRequest).toEqual({
63+
method: 'GET',
64+
url: 'http://subdomain.sentry-test-external.io/does-not-exist',
65+
});
5866
});
5967

6068
sentryTest('handles fetch invalid header name errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -85,6 +93,10 @@ sentryTest('handles fetch invalid header name errors @firefox', async ({ getLoca
8593
frames: expect.any(Array),
8694
},
8795
});
96+
expect(eventData.contexts?.outgoingRequest).toEqual({
97+
method: 'GET',
98+
url: 'http://sentry-test-external.io/invalid-header-name',
99+
});
88100
});
89101

90102
sentryTest('handles fetch invalid header value errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -117,6 +129,10 @@ sentryTest('handles fetch invalid header value errors @firefox', async ({ getLoc
117129
frames: expect.any(Array),
118130
},
119131
});
132+
expect(eventData.contexts?.outgoingRequest).toEqual({
133+
method: 'GET',
134+
url: 'http://sentry-test-external.io/invalid-header-value',
135+
});
120136
});
121137

122138
sentryTest('handles fetch invalid URL scheme errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -159,6 +175,10 @@ sentryTest('handles fetch invalid URL scheme errors @firefox', async ({ getLocal
159175
frames: expect.any(Array),
160176
},
161177
});
178+
expect(eventData.contexts?.outgoingRequest).toEqual({
179+
method: 'GET',
180+
url: 'blub://sentry-test-external.io/invalid-scheme',
181+
});
162182
});
163183

164184
sentryTest('handles fetch credentials in url errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -191,6 +211,10 @@ sentryTest('handles fetch credentials in url errors @firefox', async ({ getLocal
191211
frames: expect.any(Array),
192212
},
193213
});
214+
expect(eventData.contexts?.outgoingRequest).toEqual({
215+
method: 'GET',
216+
url: 'https://user:[email protected]/credentials-in-url',
217+
});
194218
});
195219

196220
sentryTest('handles fetch invalid mode errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -222,6 +246,10 @@ sentryTest('handles fetch invalid mode errors @firefox', async ({ getLocalTestUr
222246
frames: expect.any(Array),
223247
},
224248
});
249+
expect(eventData.contexts?.outgoingRequest).toEqual({
250+
method: 'GET',
251+
url: 'http://sentry-test-external.io/invalid-mode',
252+
});
225253
});
226254

227255
sentryTest('handles fetch invalid request method errors @firefox', async ({ getLocalTestUrl, page, browserName }) => {
@@ -252,6 +280,10 @@ sentryTest('handles fetch invalid request method errors @firefox', async ({ getL
252280
frames: expect.any(Array),
253281
},
254282
});
283+
expect(eventData.contexts?.outgoingRequest).toEqual({
284+
method: 'CONNECT',
285+
url: 'http://sentry-test-external.io/invalid-method',
286+
});
255287
});
256288

257289
sentryTest(
@@ -284,5 +316,9 @@ sentryTest(
284316
frames: expect.any(Array),
285317
},
286318
});
319+
expect(eventData.contexts?.outgoingRequest).toEqual({
320+
method: 'PUT',
321+
url: 'http://sentry-test-external.io/no-cors-method',
322+
});
287323
},
288324
);

packages/core/src/utils-hoist/instrument/fetch.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { Scope } from '../../scope';
23
import type { HandlerDataFetch } from '../../types-hoist';
4+
import { addScopeDataToError } from '../../utils/prepareEvent';
35

46
import { isError } from '../is';
57
import { addNonEnumerableProperty, fill } from '../object';
@@ -126,6 +128,15 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
126128
}
127129
}
128130

131+
// We attach an additional scope to the error, which contains the outgoing request data
132+
// If this error bubbles up and is captured by Sentry, the scope will be added to the event
133+
const scope = new Scope();
134+
scope.setContext('outgoingRequest', {
135+
method,
136+
url,
137+
});
138+
addScopeDataToError(error, scope);
139+
129140
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
130141
// it means the sentry.javascript SDK caught an error invoking your application code.
131142
// This is expected behavior and NOT indicative of a bug with sentry.javascript.

packages/core/src/utils/prepareEvent.ts

+23
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ export type ExclusiveEventHintOrCaptureContext =
2121
| (CaptureContext & Partial<{ [key in keyof EventHint]: never }>)
2222
| (EventHint & Partial<{ [key in keyof ScopeContext]: never }>);
2323

24+
const errorScopeMap = new WeakMap<Error, Scope>();
25+
26+
/**
27+
* Add a scope that should be applied to the given error, if it is captured by Sentry.
28+
*/
29+
export function addScopeDataToError(error: Error, scope: Scope): void {
30+
try {
31+
errorScopeMap.set(error, scope);
32+
} catch {
33+
// ignore it if errors happen here, e.g. if `error` is not an object
34+
}
35+
}
36+
2437
/**
2538
* Adds common information to events.
2639
*
@@ -84,6 +97,16 @@ export function prepareEvent(
8497
mergeScopeData(data, isolationData);
8598
}
8699

100+
// In some cases, additional scope data may be attached to an error
101+
// We also merge this data into the event scope data, if available
102+
const originalException = hint.originalException;
103+
if (originalException instanceof Error) {
104+
const additionalErrorScope = errorScopeMap.get(originalException);
105+
if (additionalErrorScope) {
106+
mergeScopeData(data, additionalErrorScope.getScopeData());
107+
}
108+
}
109+
87110
if (finalScope) {
88111
const finalScopeData = finalScope.getScopeData();
89112
mergeScopeData(data, finalScopeData);

0 commit comments

Comments
 (0)