diff --git a/packages/browser-utils/src/instrument/xhr.ts b/packages/browser-utils/src/instrument/xhr.ts index e97b7e54be60..ad7f81459e7b 100644 --- a/packages/browser-utils/src/instrument/xhr.ts +++ b/packages/browser-utils/src/instrument/xhr.ts @@ -37,7 +37,7 @@ export function instrumentXHR(): void { // open() should always be called with two or more arguments // But to be on the safe side, we actually validate this and bail out if we don't have a method & url const method = isString(xhrOpenArgArray[0]) ? xhrOpenArgArray[0].toUpperCase() : undefined; - const url = parseUrl(xhrOpenArgArray[1]); + const url = ensureUrlIsString(xhrOpenArgArray[1]); if (!method || !url) { return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray); @@ -140,7 +140,7 @@ export function instrumentXHR(): void { }); } -function parseUrl(url: string | unknown): string | undefined { +function ensureUrlIsString(url: string | unknown): string | undefined { if (isString(url)) { return url; } diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 9d7faeb4e9c4..14c7977c6d97 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan } from '@sentry/core'; import { setMeasurement } from '@sentry/core'; -import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/core'; +import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger } from '@sentry/core'; import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/types'; import { spanToJSON } from '@sentry/core'; @@ -545,8 +545,6 @@ export function _addResourceSpans( return; } - const parsedUrl = parseUrl(resourceUrl); - const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', }; @@ -561,12 +559,18 @@ export function _addResourceSpans( if ('renderBlockingStatus' in entry) { attributes['resource.render_blocking_status'] = entry.renderBlockingStatus; } - if (parsedUrl.protocol) { - attributes['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it. - } - if (parsedUrl.host) { - attributes['server.address'] = parsedUrl.host; + try { + // The URL constructor can throw when there is no protocol or host. + const parsedUrl = new URL(resourceUrl); + if (parsedUrl.protocol) { + attributes['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it. + } + if (parsedUrl.host) { + attributes['server.address'] = parsedUrl.host; + } + } catch { + // noop } attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin); diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 889e1a54c201..edb746f2b5e7 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -13,7 +13,6 @@ import { getEventDescription, htmlTreeAsString, logger, - parseUrl, safeJoin, severityLevelFromString, } from '@sentry/core'; @@ -328,6 +327,9 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe }; } +// Just a dummy url base for the `URL` constructor. +const DUMMY_URL_BASE = 'a://'; + /** * Creates breadcrumbs from history API calls */ @@ -337,24 +339,25 @@ function _getHistoryBreadcrumbHandler(client: Client): (handlerData: HandlerData return; } + const currentUrl = new URL(WINDOW.location.href); + let from: string | undefined = handlerData.from; let to: string | undefined = handlerData.to; - const parsedLoc = parseUrl(WINDOW.location.href); - let parsedFrom = from ? parseUrl(from) : undefined; - const parsedTo = parseUrl(to); + let parsedFrom = from ? new URL(from, DUMMY_URL_BASE) : undefined; + const parsedTo = new URL(to, DUMMY_URL_BASE); // Initial pushState doesn't provide `from` information - if (!parsedFrom || !parsedFrom.path) { - parsedFrom = parsedLoc; + if (!parsedFrom || !parsedFrom.pathname) { + parsedFrom = currentUrl; } // Use only the path component of the URL if the URL matches the current // document (almost all the time when using pushState) - if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) { - to = parsedTo.relative; + if (currentUrl.origin === parsedTo.origin) { + to = `${parsedTo.pathname}${parsedTo.search}${parsedTo.hash}`; } - if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) { - from = parsedFrom.relative; + if (currentUrl.origin === parsedFrom.origin) { + from = `${parsedTo.pathname}${parsedTo.search}${parsedTo.hash}`; } addBreadcrumb({ diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 79ed4d0f05b4..cce9cbb5759c 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -14,6 +14,7 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, getIsolationScope, + getSanitizedUrlString, hasTracingEnabled, instrumentFetchRequest, setHttpStatus, @@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial boolean, @@ -378,8 +384,14 @@ export function xhrCallback( return undefined; } - const fullUrl = getFullURL(sentryXhrData.url); - const host = fullUrl ? parseUrl(fullUrl).host : undefined; + let parsedUrl; + try { + // By adding a base URL to new URL(), this will also work for relative urls + // If `url` is a full URL, the base URL is ignored anyhow + parsedUrl = new URL(sentryXhrData.url, WINDOW.location.origin); + } catch { + // noop + } const hasParent = !!getActiveSpan(); @@ -390,9 +402,9 @@ export function xhrCallback( attributes: { type: 'xhr', 'http.method': sentryXhrData.method, - 'http.url': fullUrl, + 'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined, url: sentryXhrData.url, - 'server.address': host, + 'server.address': parsedUrl ? parsedUrl.host : undefined, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', }, @@ -455,14 +467,3 @@ function setHeaderOnXhr( // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. } } - -function getFullURL(url: string): string | undefined { - try { - // By adding a base URL to new URL(), this will also work for relative urls - // If `url` is a full URL, the base URL is ignored anyhow - const parsed = new URL(url, WINDOW.location.origin); - return parsed.href; - } catch { - return undefined; - } -} diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index f2811a026919..aeb117799f5d 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -9,7 +9,6 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; -import { extractQueryParamsFromUrl, getSanitizedUrlString, parseUrl } from '@sentry/core'; import type { IntegrationFn, RequestEventData, SpanAttributes } from '@sentry/types'; const INTEGRATION_NAME = 'BunServer'; @@ -50,6 +49,9 @@ export function instrumentBunServe(): void { }); } +// Just a dummy url base for the `URL` constructor. +const DUMMY_URL_BASE = 'a://'; + /** * Instruments Bun.serve `fetch` option to automatically create spans and capture errors. */ @@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] return fetchTarget.apply(fetchThisArg, fetchArgs); } - const parsedUrl = parseUrl(request.url); const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: request.method || 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }; + + const parsedUrl = new URL(request.url, DUMMY_URL_BASE); + if (parsedUrl.search) { attributes['http.query'] = parsedUrl.search; } - const url = getSanitizedUrlString(parsedUrl); - isolationScope.setSDKProcessingMetadata({ normalizedRequest: { - url, + url: `${parsedUrl.pathname}${parsedUrl.search}`, method: request.method, headers: request.headers.toJSON(), - query_string: extractQueryParamsFromUrl(url), } satisfies RequestEventData, }); @@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] { attributes, op: 'http.server', - name: `${request.method} ${parsedUrl.path || '/'}`, + name: `${request.method} ${parsedUrl.pathname || '/'}`, }, async span => { try { diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 5880d2594baa..bd4516007753 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -16,7 +16,6 @@ import { } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; import { generateSentryTraceHeader } from './utils-hoist/tracing'; -import { parseUrl } from './utils-hoist/url'; import { hasTracingEnabled } from './utils/hasTracingEnabled'; import { getActiveSpan, spanToTraceHeader } from './utils/spanUtils'; @@ -68,8 +67,12 @@ export function instrumentFetchRequest( const { method, url } = handlerData.fetchData; - const fullUrl = getFullURL(url); - const host = fullUrl ? parseUrl(fullUrl).host : undefined; + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch { + // noop + } const hasParent = !!getActiveSpan(); @@ -81,8 +84,8 @@ export function instrumentFetchRequest( url, type: 'fetch', 'http.method': method, - 'http.url': fullUrl, - 'server.address': host, + 'http.url': parsedUrl ? parsedUrl.href : undefined, + 'server.address': parsedUrl ? parsedUrl.hostname : undefined, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', }, @@ -227,15 +230,6 @@ export function addTracingHeadersToFetchRequest( } } -function getFullURL(url: string): string | undefined { - try { - const parsed = new URL(url); - return parsed.href; - } catch { - return undefined; - } -} - function endSpan(span: Span, handlerData: HandlerDataFetch): void { if (handlerData.response) { setHttpStatus(span, handlerData.response.status); diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 1625ea6c0868..51e7ceea69eb 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -151,6 +151,7 @@ export { parseBaggageHeader, } from './baggage'; +// eslint-disable-next-line deprecation/deprecation export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; export { makeFifoCache } from './cache'; export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils-hoist/url.ts index e324f41f82a3..8ee967bcde16 100644 --- a/packages/core/src/utils-hoist/url.ts +++ b/packages/core/src/utils-hoist/url.ts @@ -1,6 +1,7 @@ type PartialURL = { host?: string; path?: string; + pathname?: string; protocol?: string; relative?: string; search?: string; @@ -13,6 +14,8 @@ type PartialURL = { * // intentionally using regex and not href parsing trick because React Native and other * // environments where DOM might not be available * @returns parsed URL object + * + * @deprecated This function is deprecated and will be removed in the next major version. Use `new URL()` instead. */ export function parseUrl(url: string): PartialURL { if (!url) { @@ -61,7 +64,10 @@ export function getNumberOfUrlSegments(url: string): number { * see: https://develop.sentry.dev/sdk/data-handling/#structuring-data */ export function getSanitizedUrlString(url: PartialURL): string { - const { protocol, host, path } = url; + const { protocol, host, path, pathname } = url; + + // This is the compatibility layer between PartialURL and URL + const prioritizedPathArg = pathname || path; const filteredHost = (host && @@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string { .replace(/(:443)$/, '')) || ''; - return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`; + return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`; } diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index fd8b861516ab..0083b58b92fb 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -84,6 +84,7 @@ describe('getSanitizedUrlString', () => { ['url with port 443', 'http://172.31.12.144:443/test', 'http://172.31.12.144/test'], ['url with IP and port 80', 'http://172.31.12.144:80/test', 'http://172.31.12.144/test'], ])('returns a sanitized URL for a %s', (_, rawUrl: string, sanitizedURL: string) => { + // eslint-disable-next-line deprecation/deprecation const urlObject = parseUrl(rawUrl); expect(getSanitizedUrlString(urlObject)).toEqual(sanitizedURL); }); diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 4d948c9d37c4..7331ca67f99b 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -13,7 +13,6 @@ import { getSanitizedUrlString, httpRequestToRequestData, logger, - parseUrl, stripUrlQueryAndFragment, withIsolationScope, } from '@sentry/core'; @@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi function getBreadcrumbData(request: http.ClientRequest): Partial { try { // `request.host` does not contain the port, but the host header does - const host = request.getHeader('host') || request.host; + const hostHeader = request.getHeader('host'); + const host = typeof hostHeader === 'string' ? hostHeader : request.host; const url = new URL(request.path, `${request.protocol}//${host}`); - const parsedUrl = parseUrl(url.toString()); const data: Partial = { - url: getSanitizedUrlString(parsedUrl), + url: getSanitizedUrlString(url), 'http.method': request.method || 'GET', }; - if (parsedUrl.search) { - data['http.query'] = parsedUrl.search; + if (url.search) { + data['http.query'] = url.search; } - if (parsedUrl.hash) { - data['http.fragment'] = parsedUrl.hash; + if (url.hash) { + data['http.fragment'] = url.hash; } return data; diff --git a/packages/node/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts index d02fa53f789f..2bb4e22cd603 100644 --- a/packages/node/src/integrations/node-fetch.ts +++ b/packages/node/src/integrations/node-fetch.ts @@ -8,7 +8,7 @@ import { getCurrentScope, hasTracingEnabled, } from '@sentry/core'; -import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, parseUrl } from '@sentry/core'; +import { getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString } from '@sentry/core'; import { addOpenTelemetryInstrumentation, generateSpanContextForPropagationContext, @@ -128,18 +128,17 @@ function addRequestBreadcrumb(request: UndiciRequest, response: UndiciResponse): function getBreadcrumbData(request: UndiciRequest): Partial { try { const url = new URL(request.path, request.origin); - const parsedUrl = parseUrl(url.toString()); const data: Partial = { - url: getSanitizedUrlString(parsedUrl), + url: getSanitizedUrlString(url), 'http.method': request.method || 'GET', }; - if (parsedUrl.search) { - data['http.query'] = parsedUrl.search; + if (url.search) { + data['http.query'] = url.search; } - if (parsedUrl.hash) { - data['http.fragment'] = parsedUrl.hash; + if (url.hash) { + data['http.fragment'] = url.hash; } return data; diff --git a/packages/opentelemetry/src/utils/getRequestSpanData.ts b/packages/opentelemetry/src/utils/getRequestSpanData.ts index 1ba4e374fc6c..d44884ddcf82 100644 --- a/packages/opentelemetry/src/utils/getRequestSpanData.ts +++ b/packages/opentelemetry/src/utils/getRequestSpanData.ts @@ -6,13 +6,16 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL, } from '@opentelemetry/semantic-conventions'; -import { getSanitizedUrlString, parseUrl } from '@sentry/core'; +import { getSanitizedUrlString } from '@sentry/core'; import type { SanitizedRequestData } from '@sentry/types'; import { spanHasAttributes } from './spanTypes'; +// Just a dummy url base for the `URL` constructor. +const DUMMY_URL_BASE = 'dummy://'; + /** - * Get sanitizied request data from an OTEL span. + * Get sanitized request data from an OTEL span. */ export function getRequestSpanData(span: Span | ReadableSpan): Partial { // The base `Span` type has no `attributes`, so we need to guard here against that @@ -40,9 +43,14 @@ export function getRequestSpanData(span: Span | ReadableSpan): Partial