Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
Expand DownExpand Up@@ -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;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/browser-utils/src/metrics/browserMetrics.ts
Original file line numberDiff line numberDiff line change
@@ -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';
Expand DownExpand Up@@ -545,8 +545,6 @@ export function _addResourceSpans(
return;
}

const parsedUrl = parseUrl(resourceUrl);

const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
};
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can resourceUrl be a path-only, without a host? 🤔 just double checking.

if (parsedUrl.protocol) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can protocol be empty?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can host even be empty? 🤔

attributes['server.address'] = parsedUrl.host;
}
} catch {
// noop
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down
23 changes: 13 additions & 10 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getEventDescription,
htmlTreeAsString,
logger,
parseUrl,
safeJoin,
severityLevelFromString,
} from '@sentry/core';
Expand DownExpand Up@@ -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
*/
Expand All@@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always try-catch this, to avoid stuff blowing up IMHO!


// 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({
Expand Down
45 changes: 23 additions & 22 deletions packages/browser/src/tracing/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ import {
getDynamicSamplingContextFromClient,
getDynamicSamplingContextFromSpan,
getIsolationScope,
getSanitizedUrlString,
hasTracingEnabled,
instrumentFetchRequest,
setHttpStatus,
Expand DownExpand Up@@ -173,15 +174,19 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
responseToSpanId.set(handlerData.response, handlerData.fetchData.__span);
}

// We cannot use `window.location` in the generic fetch instrumentation,
// but we need it for reliable `server.address` attribute.
// so we extend this in here
if (createdSpan) {
const fullUrl = getFullURL(handlerData.fetchData.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(handlerData.fetchData.url, WINDOW.location.origin);
} catch {
// noop
}

createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
'http.url': parsedUrl ? getSanitizedUrlString(parsedUrl) : undefined,
'server.address': parsedUrl ? parsedUrl.host : undefined,
});
}

Expand DownExpand Up@@ -347,6 +352,7 @@ export function shouldAttachHeaders(
*
* @returns Span if a span was created, otherwise void.
*/
// eslint-disable-next-line complexity
export function xhrCallback(
handlerData: HandlerDataXhr,
shouldCreateSpan: (url: string) => boolean,
Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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;
}
}
15 changes: 8 additions & 7 deletions packages/bun/src/integrations/bunserver.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand DownExpand Up@@ -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.
*/
Expand All@@ -63,24 +65,23 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try-catch, maybe?


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,
});

Expand All@@ -91,7 +92,7 @@ function instrumentBunServeOptions(serveOptions: Parameters<typeof Bun.serve>[0]
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
name: `${request.method} ${parsedUrl.pathname || '/'}`,
},
async span => {
try {
Expand Down
22 changes: 8 additions & 14 deletions packages/core/src/fetch.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';

Expand DownExpand Up@@ -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();

Expand All@@ -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',
},
Expand DownExpand Up@@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils-hoist/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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';
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils-hoist/url.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
type PartialURL = {
host?: string;
path?: string;
pathname?: string;
protocol?: string;
relative?: string;
search?: string;
Expand All@@ -13,6 +14,8 @@ type PartialURL = {
* // intentionally using regex and not <a/> 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) {
Expand DownExpand Up@@ -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 &&
Expand All@@ -74,5 +80,5 @@ export function getSanitizedUrlString(url: PartialURL): string {
.replace(/(:443)$/, '')) ||
'';

return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`;
return `${protocol ? `${protocol}://` : ''}${filteredHost}${prioritizedPathArg}`;
}
1 change: 1 addition & 0 deletions packages/core/test/utils-hoist/url.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);
});
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,6 @@ import {
getSanitizedUrlString,
httpRequestToRequestData,
logger,
parseUrl,
stripUrlQueryAndFragment,
withIsolationScope,
} from '@sentry/core';
Expand DownExpand Up@@ -311,20 +310,20 @@ function addRequestBreadcrumb(request: http.ClientRequest, response: http.Incomi
function getBreadcrumbData(request: http.ClientRequest): Partial<SanitizedRequestData> {
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<SanitizedRequestData> = {
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;
Expand Down
Loading