From a3f90b5162387a3c7b740509dfe9772148222d87 Mon Sep 17 00:00:00 2001 From: Kunal Vashisht Date: Tue, 27 Oct 2026 20:31:59 +0000 Subject: [PATCH 1/3] fix(browser): Resolve request URLs against location.href in tracing --- packages/browser/src/tracing/request.ts | 3 +- packages/browser/src/tracing/utils.ts | 4 +- packages/browser/test/tracing/request.test.ts | 87 ++++++++++++++++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index dd5cdb351a3c..87982bd83f02 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -155,7 +155,8 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { }); it('resolves a relative fetch URL against the page origin for the streamed span name', () => { - vi.stubGlobal('location', { origin: 'https://app.example.com' }); + vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' }); let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined; let requestSpan: utils.Span | undefined; @@ -261,7 +261,7 @@ describe('instrumentOutgoingRequests', () => { }); it('resolves a relative XHR URL against the page origin for the streamed span name', () => { - vi.stubGlobal('location', { origin: 'https://app.example.com' }); + vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' }); let xhrHandler: ((data: utils.HandlerDataXhr) => void) | undefined; let requestSpan: utils.Span | undefined; @@ -299,6 +299,89 @@ describe('instrumentOutgoingRequests', () => { vi.unstubAllGlobals(); }); + describe('resolving request URLs against location.href', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.each([ + { + name: 'an absolute URL on a page with an opaque origin', + location: { origin: 'null', href: 'about:srcdoc' }, + requestUrl: 'https://api.example.org/v1/users', + expected: 'https://api.example.org/v1/users', + }, + { + name: 'a relative path without a leading slash', + location: { origin: 'https://app.example.com', href: 'https://app.example.com/dashboard/settings' }, + requestUrl: 'users', + expected: 'https://app.example.com/dashboard/users', + }, + ])('sets url.full on XHR spans for $name', ({ location, requestUrl, expected }) => { + vi.stubGlobal('location', location); + + let xhrHandler: ((data: utils.HandlerDataXhr) => void) | undefined; + let requestSpan: utils.Span | undefined; + + vi.spyOn(browserUtils, 'addXhrInstrumentationHandler').mockImplementation(handler => { + xhrHandler = handler; + }); + const tracingClient = new BrowserClient(getDefaultBrowserClientOptions({ tracesSampleRate: 1 })); + utils.setCurrentClient(tracingClient); + utils._INTERNAL_setSpanForScope(utils.getCurrentScope(), new utils.SentrySpan({ sampled: true })); + + instrumentOutgoingRequests(tracingClient, { + traceFetch: false, + enableHTTPTimings: false, + onRequestSpanStart: span => { + requestSpan = span; + }, + }); + xhrHandler?.({ + xhr: { + [browserUtils.SENTRY_XHR_DATA_KEY]: { + method: 'GET', + url: requestUrl, + request_headers: {}, + }, + setRequestHeader: vi.fn(), + }, + startTimestamp: Date.now(), + } as utils.HandlerDataXhr); + + expect(utils.spanToJSON(requestSpan!).attributes['url.full']).toBe(expected); + }); + + it('sets url.domain on fetch spans for an absolute URL on a page with an opaque origin', () => { + vi.stubGlobal('location', { origin: 'null', href: 'about:srcdoc' }); + + let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined; + let requestSpan: utils.Span | undefined; + + vi.spyOn(utils, 'addFetchInstrumentationHandler').mockImplementation(handler => { + fetchHandler = handler; + }); + const tracingClient = new BrowserClient(getDefaultBrowserClientOptions({ tracesSampleRate: 1 })); + utils.setCurrentClient(tracingClient); + utils._INTERNAL_setSpanForScope(utils.getCurrentScope(), new utils.SentrySpan({ sampled: true })); + + instrumentOutgoingRequests(tracingClient, { + traceXHR: false, + enableHTTPTimings: false, + onRequestSpanStart: span => { + requestSpan = span; + }, + }); + fetchHandler?.({ + fetchData: { method: 'GET', url: 'https://api.example.org/v1/users' }, + args: ['https://api.example.org/v1/users'], + startTimestamp: Date.now(), + }); + + expect(utils.spanToJSON(requestSpan!).attributes['url.domain']).toBe('api.example.org'); + }); + }); + it('falls back to the request method for a data URL, which has no domain', () => { let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined; let requestSpan: utils.Span | undefined; From 4a140b378a84b53b46b17c5f010c5c76036aba6d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 28 Oct 2026 12:08:18 +0100 Subject: [PATCH 2/3] ref(browser): Address review comments Remove redundant comments and use @sentry/conventions constants for span attributes in request tests. Co-Authored-By: Claude Opus 5.5 --- packages/browser/src/tracing/request.ts | 1 - packages/browser/src/tracing/utils.ts | 2 -- packages/browser/test/tracing/request.test.ts | 16 ++++++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 87982bd83f02..c4f9228a5337 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -155,7 +155,6 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { const requestSpanJson = utils.spanToJSON(requestSpan!); expect(requestSpanJson.name).toBe('GET example.com'); - expect(requestSpanJson.attributes['url.domain']).toBe('example.com'); - expect(requestSpanJson.attributes['server.address']).toBe('example.com:8443'); + expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('example.com'); + expect(requestSpanJson.attributes[SERVER_ADDRESS]).toBe('example.com:8443'); }); it('resolves a relative fetch URL against the page origin for the streamed span name', () => { @@ -255,7 +255,7 @@ describe('instrumentOutgoingRequests', () => { const requestSpanJson = utils.spanToJSON(requestSpan!); expect(requestSpanJson.name).toBe('GET app.example.com'); - expect(requestSpanJson.attributes['url.domain']).toBe('app.example.com'); + expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('app.example.com'); vi.unstubAllGlobals(); }); @@ -294,7 +294,7 @@ describe('instrumentOutgoingRequests', () => { const requestSpanJson = utils.spanToJSON(requestSpan!); expect(requestSpanJson.name).toBe('GET app.example.com'); - expect(requestSpanJson.attributes['url.domain']).toBe('app.example.com'); + expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('app.example.com'); vi.unstubAllGlobals(); }); @@ -349,7 +349,7 @@ describe('instrumentOutgoingRequests', () => { startTimestamp: Date.now(), } as utils.HandlerDataXhr); - expect(utils.spanToJSON(requestSpan!).attributes['url.full']).toBe(expected); + expect(utils.spanToJSON(requestSpan!).attributes[URL_FULL]).toBe(expected); }); it('sets url.domain on fetch spans for an absolute URL on a page with an opaque origin', () => { @@ -378,7 +378,7 @@ describe('instrumentOutgoingRequests', () => { startTimestamp: Date.now(), }); - expect(utils.spanToJSON(requestSpan!).attributes['url.domain']).toBe('api.example.org'); + expect(utils.spanToJSON(requestSpan!).attributes[URL_DOMAIN]).toBe('api.example.org'); }); }); @@ -408,7 +408,7 @@ describe('instrumentOutgoingRequests', () => { const requestSpanJson = utils.spanToJSON(requestSpan!); expect(requestSpanJson.name).toBe('GET'); - expect(requestSpanJson.attributes['url.domain']).toBeUndefined(); + expect(requestSpanJson.attributes[URL_DOMAIN]).toBeUndefined(); }); describe('XHR trace header span', () => { From 0c16d6ecbc2a981b5980f468ee87268251a8b5af Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 28 Oct 2026 12:27:19 +0100 Subject: [PATCH 3/3] fix(browser): Resolve streamed fetch span URLs against location.href The http.client.stream spans had the same opaque-origin issue as the regular request spans. Also update comments and test names that still referred to the page origin. Co-Authored-By: Claude Opus 5.5 --- .../integrations/fetchStreamPerformance.ts | 2 +- packages/browser/src/tracing/request.ts | 2 +- .../fetchStreamPerformance.test.ts | 24 ++++++++++++++----- packages/browser/test/tracing/request.test.ts | 4 ++-- packages/core/src/fetch.ts | 2 +- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/browser/src/integrations/fetchStreamPerformance.ts b/packages/browser/src/integrations/fetchStreamPerformance.ts index c021c959e883..735746fe9ac7 100644 --- a/packages/browser/src/integrations/fetchStreamPerformance.ts +++ b/packages/browser/src/integrations/fetchStreamPerformance.ts @@ -81,7 +81,7 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => { : url; // `http.client.stream` follows the same name rules as `http.client`. - const domain = getUrlDomain(url, WINDOW.location?.origin); + const domain = getUrlDomain(url, WINDOW.location?.href); const streamedName = domain ? `${method} ${domain}` : method; const streamSpan = startInactiveSpan({ name: hasSpanStreamingEnabled(client) ? streamedName : `${method} ${sanitizedUrl}`, diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index c4f9228a5337..31e1b831218e 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -376,7 +376,7 @@ function xhrCallback( // `parseUrl` returns the raw authority — userinfo credentials must never reach an attribute. const host = parsedUrl?.host?.replace(/^.*@/, ''); - // `getFullURL` already resolved relative URLs against the page origin; only data URLs have no domain. + // `getFullURL` already resolved relative URLs against the page URL; only data URLs have no domain. const domain = getUrlDomain(fullUrl || url); // With span streaming, span names have to be low cardinality, so only the domain is kept. diff --git a/packages/browser/test/integrations/fetchStreamPerformance.test.ts b/packages/browser/test/integrations/fetchStreamPerformance.test.ts index b74919be9430..dcc5bdc2b86c 100644 --- a/packages/browser/test/integrations/fetchStreamPerformance.test.ts +++ b/packages/browser/test/integrations/fetchStreamPerformance.test.ts @@ -1,13 +1,14 @@ import type { Client, HandlerDataFetch } from '@sentry/core'; import * as utils from '@sentry/core'; import * as coreBrowser from '@sentry/core/browser'; +import { URL_DOMAIN } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { fetchStreamPerformanceIntegration } from '../../src/integrations/fetchStreamPerformance'; describe('fetchStreamPerformanceIntegration', () => { beforeEach(() => { vi.clearAllMocks(); - vi.stubGlobal('location', { origin: 'https://app.example.com' }); + vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' }); }); afterEach(() => { @@ -48,16 +49,27 @@ describe('fetchStreamPerformanceIntegration', () => { expect(trackStreamedFetch('stream', 'https://api.example.com/v1/chat?stream=1')).toHaveBeenCalledWith( expect.objectContaining({ name: 'GET api.example.com', - attributes: expect.objectContaining({ 'url.domain': 'api.example.com' }), + attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }), }), ); }); - it('resolves a relative URL against the page origin', () => { + it('resolves a relative URL against the page URL', () => { expect(trackStreamedFetch('stream', '/v1/chat')).toHaveBeenCalledWith( expect.objectContaining({ name: 'GET app.example.com', - attributes: expect.objectContaining({ 'url.domain': 'app.example.com' }), + attributes: expect.objectContaining({ [URL_DOMAIN]: 'app.example.com' }), + }), + ); + }); + + it('keeps the domain of an absolute URL on a page with an opaque origin', () => { + vi.stubGlobal('location', { origin: 'null', href: 'about:srcdoc' }); + + expect(trackStreamedFetch('stream', 'https://api.example.com/v1/chat')).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'GET api.example.com', + attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }), }), ); }); @@ -66,7 +78,7 @@ describe('fetchStreamPerformanceIntegration', () => { expect(trackStreamedFetch('stream', 'data:text/event-stream,data: hi')).toHaveBeenCalledWith( expect.objectContaining({ name: 'GET', - attributes: expect.objectContaining({ 'url.domain': undefined }), + attributes: expect.objectContaining({ [URL_DOMAIN]: undefined }), }), ); }); @@ -75,7 +87,7 @@ describe('fetchStreamPerformanceIntegration', () => { expect(trackStreamedFetch('static', 'https://api.example.com/v1/chat?stream=1')).toHaveBeenCalledWith( expect.objectContaining({ name: 'GET https://api.example.com/v1/chat', - attributes: expect.objectContaining({ 'url.domain': 'api.example.com' }), + attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }), }), ); }); diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index eb56d3bd74c9..e0c8fe8f0947 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -227,7 +227,7 @@ describe('instrumentOutgoingRequests', () => { expect(requestSpanJson.attributes[SERVER_ADDRESS]).toBe('example.com:8443'); }); - it('resolves a relative fetch URL against the page origin for the streamed span name', () => { + it('resolves a relative fetch URL against the page URL for the streamed span name', () => { vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' }); let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined; @@ -260,7 +260,7 @@ describe('instrumentOutgoingRequests', () => { vi.unstubAllGlobals(); }); - it('resolves a relative XHR URL against the page origin for the streamed span name', () => { + it('resolves a relative XHR URL against the page URL for the streamed span name', () => { vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' }); let xhrHandler: ((data: utils.HandlerDataXhr) => void) | undefined; diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 76ef3892bea1..314aeac57e71 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -52,7 +52,7 @@ interface InstrumentFetchRequestOptions { spanOrigin?: SpanOrigin; propagateTraceparent?: boolean; onRequestSpanEnd?: (span: Span, responseInformation: ResponseHookInfo) => void; - /** Base URL for relative request URLs. Browsers pass the page origin; server runtimes have none. */ + /** Base URL for relative request URLs. Browsers pass the page URL; server runtimes have none. */ urlBase?: string; }