From ec9e7e2f7a3c9a0b81bb850cebea18934d78d37b Mon Sep 17 00:00:00 2001 From: Owen Donckers Date: Wed, 12 Aug 2026 11:11:57 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Resolve=20document-relative=20UR?= =?UTF-8?q?Ls=20against=20document.baseURI=20in=20normalizeUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeUrl resolved relative URLs against location.href, but the browser resolves relative request URLs against the document base URI (). On a route deeper than the base href, the URL recorded for a fetch/XHR differed from the one actually requested, so the resource event never paired with its PerformanceResourceTiming entry — dropping method, status_code, _dd.trace_id and _dd.span_id and breaking RUM → APM correlation. Resolve against document.baseURI, falling back to location.href where there is no document (workers, SSR). Absolute and root-relative inputs are unaffected. --- .../src/tools/utils/urlPolyfill.spec.ts | 30 +++++++++++++++++++ packages/js-core/src/util/urlPolyfill.ts | 12 ++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts b/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts index 4ca746ffb5..a8828737cf 100644 --- a/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts +++ b/packages/browser-core/src/tools/utils/urlPolyfill.spec.ts @@ -29,6 +29,36 @@ describe('normalize url', () => { // let's check for both. expect(['file:///my/path', 'file://foo.com/my/path']).toContain(normalizeUrl('file://foo.com/my/path')) }) + + describe('with a differing from the current path', () => { + // The browser resolves document-relative request URLs against the document base URI, not the + // page location. normalizeUrl must match that so the recorded URL pairs with its + // PerformanceResourceTiming entry. + let base: HTMLBaseElement + + beforeEach(() => { + history.pushState({}, '', '/deep/route') + base = document.createElement('base') + base.href = '/' + document.head.appendChild(base) + }) + + afterEach(() => { + base.remove() + }) + + it('should resolve document-relative paths against the base URI', () => { + expect(normalizeUrl('api/foo')).toEqual(`${location.origin}/api/foo`) + }) + + it('should still resolve root-relative paths against the origin', () => { + expect(normalizeUrl('/api/foo')).toEqual(`${location.origin}/api/foo`) + }) + + it('should keep absolute urls unchanged', () => { + expect(normalizeUrl('https://foo.com/my/path')).toEqual('https://foo.com/my/path') + }) + }) }) describe('isValidUrl', () => { diff --git a/packages/js-core/src/util/urlPolyfill.ts b/packages/js-core/src/util/urlPolyfill.ts index e30d87b177..168ff3ce6f 100644 --- a/packages/js-core/src/util/urlPolyfill.ts +++ b/packages/js-core/src/util/urlPolyfill.ts @@ -1,9 +1,17 @@ import type { GlobalObject } from './globalObject' import { globalObject } from './globalObject' -/** Resolves a URL against the current page location, returning a normalized absolute URL string. */ +/** + * Resolves a URL, returning a normalized absolute URL string. + * + * Document-relative inputs are resolved against `document.baseURI` (the ``, defaulting to + * the document location) to match how the browser resolves relative request URLs — so the URL + * recorded for a fetch/XHR matches the one actually requested. Falls back to `location.href` in + * environments without a document (workers, SSR). Absolute and root-relative inputs ignore the base + * and are unaffected. + */ export function normalizeUrl(url: string) { - return buildUrl(url, globalObject.location?.href).href + return buildUrl(url, globalObject.document?.baseURI ?? globalObject.location?.href).href } /** Returns true if the given string is a valid URL. */