diff --git a/packages/core/src/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts index 443c71095e67..613262caee67 100644 --- a/packages/core/src/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -136,16 +136,18 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void): void { error.message === 'NetworkError when attempting to fetch resource.') ) { try { - const url = new URL(handlerData.fetchData.url); + const url = new URL(handlerData.fetchData.url, GLOBAL_OBJ.location?.href); const hostname = url.host; - if (enhanceOption === 'always') { - // Modify the error message directly - error.message = `${error.message} (${hostname})`; - } else { - // Store hostname as non-enumerable property for Sentry-only enhancement - // This preserves the original error message for third-party packages - addNonEnumerableProperty(error, '__sentry_fetch_url_host__', hostname); + if (hostname) { + if (enhanceOption === 'always') { + // Modify the error message directly + error.message = `${error.message} (${hostname})`; + } else { + // Store hostname as non-enumerable property for Sentry-only enhancement + // This preserves the original error message for third-party packages + addNonEnumerableProperty(error, '__sentry_fetch_url_host__', hostname); + } } } catch { // ignore it if errors happen here diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index 597174c3f94d..2ca3d60bc21d 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -19,6 +19,7 @@ import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ export type InternalGlobal = { navigator?: { userAgent?: string; maxTouchPoints?: number }; + location?: { href?: string }; console: Console; PerformanceObserver?: any; Sentry?: any; diff --git a/packages/core/test/lib/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts index e0f31ca02c82..e094f8c46760 100644 --- a/packages/core/test/lib/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -121,4 +121,40 @@ describe('instrument > addFetchInstrumentationHandler', () => { expect(error.message).toBe('Failed to fetch (example.com)'); }); + + describe('relative URLs', () => { + const globalWithLocation = GLOBAL_OBJ as typeof GLOBAL_OBJ & { location?: { href: string } }; + + afterEach(() => { + Reflect.deleteProperty(globalWithLocation, 'location'); + }); + + async function failWith(target: string): Promise { + const error = new TypeError('Failed to fetch'); + globalThis.fetch = vi.fn().mockRejectedValue(error); + addFetchInstrumentationHandler(() => undefined); + await expect(globalThis.fetch(target)).rejects.toBe(error); + return error; + } + + it.each([ + ['a relative path', 'https://app.example.com/some/page?x=1', '/api/users', 'Failed to fetch (app.example.com)'], + ['an empty URL (`fetch("")`)', 'https://app.example.com/some/page?x=1', '', 'Failed to fetch (app.example.com)'], + ['a relative path with no current page', undefined, '/api/users', 'Failed to fetch'], + ['a relative path on a `file://` page', 'file:///C:/app/index.html', '/api/users', 'Failed to fetch'], + ['a relative path with an opaque origin', 'null', '/api/users', 'Failed to fetch'], + [ + 'an absolute URL, ignoring the current page', + 'https://app.example.com/some/page', + 'https://other.example.org/x', + 'Failed to fetch (other.example.org)', + ], + ])('handles %s', async (_name, href, target, expected) => { + if (href) { + globalWithLocation.location = { href }; + } + + expect((await failWith(target)).message).toBe(expected); + }); + }); });