From c4f3edf72ce03faa1ed8c6c3a4071ed7dc6b0388 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Mar 2025 12:22:12 +0100 Subject: [PATCH] feat(browser): Add `outgoingRequest` context to fetch errors For now, this only adds `method` and `url` fields. --- .../suites/errors/fetch/subject.js | 2 +- .../suites/errors/fetch/test.ts | 36 +++++++++++++++++++ .../core/src/utils-hoist/instrument/fetch.ts | 11 ++++++ packages/core/src/utils/prepareEvent.ts | 23 ++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch/subject.js b/dev-packages/browser-integration-tests/suites/errors/fetch/subject.js index 8bae73df7b31..da95084cce55 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch/subject.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch/subject.js @@ -31,7 +31,7 @@ window.credentialsInUrl = () => { // Invalid mode window.invalidMode = () => { - fetch('https://sentry-test-external.io/invalid-mode', { mode: 'navigate' }); + fetch('http://sentry-test-external.io/invalid-mode', { mode: 'navigate' }); }; // Invalid request method diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch/test.ts b/dev-packages/browser-integration-tests/suites/errors/fetch/test.ts index f9b59dd07f60..5760042e7809 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/errors/fetch/test.ts @@ -27,6 +27,10 @@ sentryTest('handles fetch network errors @firefox', async ({ getLocalTestUrl, pa type: 'onunhandledrejection', }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'http://sentry-test-external.io/does-not-exist', + }); }); @@ -55,6 +59,10 @@ sentryTest('handles fetch network errors on subdomains @firefox', async ({ getLo type: 'onunhandledrejection', }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'http://subdomain.sentry-test-external.io/does-not-exist', + }); }); 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 frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'http://sentry-test-external.io/invalid-header-name', + }); }); 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 frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'http://sentry-test-external.io/invalid-header-value', + }); }); 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 frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'blub://sentry-test-external.io/invalid-scheme', + }); }); 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 frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'https://user:password@sentry-test-external.io/credentials-in-url', + }); }); sentryTest('handles fetch invalid mode errors @firefox', async ({ getLocalTestUrl, page, browserName }) => { @@ -222,6 +246,10 @@ sentryTest('handles fetch invalid mode errors @firefox', async ({ getLocalTestUr frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'GET', + url: 'http://sentry-test-external.io/invalid-mode', + }); }); 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 frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'CONNECT', + url: 'http://sentry-test-external.io/invalid-method', + }); }); sentryTest( @@ -284,5 +316,9 @@ sentryTest( frames: expect.any(Array), }, }); + expect(eventData.contexts?.outgoingRequest).toEqual({ + method: 'PUT', + url: 'http://sentry-test-external.io/no-cors-method', + }); }, ); diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts index 71c5148fae9c..ba79f1b882a4 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/utils-hoist/instrument/fetch.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Scope } from '../../scope'; import type { HandlerDataFetch } from '../../types-hoist'; +import { addScopeDataToError } from '../../utils/prepareEvent'; import { isError } from '../is'; import { addNonEnumerableProperty, fill } from '../object'; @@ -126,6 +128,15 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat } } + // We attach an additional scope to the error, which contains the outgoing request data + // If this error bubbles up and is captured by Sentry, the scope will be added to the event + const scope = new Scope(); + scope.setContext('outgoingRequest', { + method, + url, + }); + addScopeDataToError(error, scope); + // NOTE: If you are a Sentry user, and you are seeing this stack frame, // it means the sentry.javascript SDK caught an error invoking your application code. // This is expected behavior and NOT indicative of a bug with sentry.javascript. diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index e417640a387a..94f42a497ff3 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -21,6 +21,19 @@ export type ExclusiveEventHintOrCaptureContext = | (CaptureContext & Partial<{ [key in keyof EventHint]: never }>) | (EventHint & Partial<{ [key in keyof ScopeContext]: never }>); +const errorScopeMap = new WeakMap(); + +/** + * Add a scope that should be applied to the given error, if it is captured by Sentry. + */ +export function addScopeDataToError(error: Error, scope: Scope): void { + try { + errorScopeMap.set(error, scope); + } catch { + // ignore it if errors happen here, e.g. if `error` is not an object + } +} + /** * Adds common information to events. * @@ -84,6 +97,16 @@ export function prepareEvent( mergeScopeData(data, isolationData); } + // In some cases, additional scope data may be attached to an error + // We also merge this data into the event scope data, if available + const originalException = hint.originalException; + if (originalException instanceof Error) { + const additionalErrorScope = errorScopeMap.get(originalException); + if (additionalErrorScope) { + mergeScopeData(data, additionalErrorScope.getScopeData()); + } + } + if (finalScope) { const finalScopeData = finalScope.getScopeData(); mergeScopeData(data, finalScopeData);