Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => {
: url;

// `http.client.stream` follows the same name rules as `http.client`.
const domain = getUrlDomain(url, WINDOW.location?.origin);
const domain = getUrlDomain(url, WINDOW.location?.href);
const streamedName = domain ? `${method} ${domain}` : method;
const streamSpan = startInactiveSpan({
name: hasSpanStreamingEnabled(client) ? streamedName : `${method} ${sanitizedUrl}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
propagateTraceparent,
onRequestSpanEnd,
// The generic fetch instrumentation has no page origin to resolve relative URLs against.
urlBase: WINDOW.location?.origin,
urlBase: WINDOW.location?.href,
});

// We cannot use `window.location` in the generic fetch instrumentation,
Expand Down Expand Up @@ -376,7 +376,7 @@ function xhrCallback(

// `parseUrl` returns the raw authority — userinfo credentials must never reach an attribute.
const host = parsedUrl?.host?.replace(/^.*@/, '');
// `getFullURL` already resolved relative URLs against the page origin; only data URLs have no domain.
// `getFullURL` already resolved relative URLs against the page URL; only data URLs have no domain.
const domain = getUrlDomain(fullUrl || url);

// With span streaming, span names have to be low cardinality, so only the domain is kept.
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/tracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export 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);
const parsed = new URL(url, WINDOW.location?.href);
return parsed.href;
} catch {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Client, HandlerDataFetch } from '@sentry/core';
import * as utils from '@sentry/core';
import * as coreBrowser from '@sentry/core/browser';
import { URL_DOMAIN } from '@sentry/conventions/attributes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchStreamPerformanceIntegration } from '../../src/integrations/fetchStreamPerformance';

describe('fetchStreamPerformanceIntegration', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal('location', { origin: 'https://app.example.com' });
vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' });
});

afterEach(() => {
Expand Down Expand Up @@ -48,16 +49,27 @@ describe('fetchStreamPerformanceIntegration', () => {
expect(trackStreamedFetch('stream', 'https://api.example.com/v1/chat?stream=1')).toHaveBeenCalledWith(
expect.objectContaining({
name: 'GET api.example.com',
attributes: expect.objectContaining({ 'url.domain': 'api.example.com' }),
attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }),
}),
);
});

it('resolves a relative URL against the page origin', () => {
it('resolves a relative URL against the page URL', () => {
expect(trackStreamedFetch('stream', '/v1/chat')).toHaveBeenCalledWith(
expect.objectContaining({
name: 'GET app.example.com',
attributes: expect.objectContaining({ 'url.domain': 'app.example.com' }),
attributes: expect.objectContaining({ [URL_DOMAIN]: 'app.example.com' }),
}),
);
});

it('keeps the domain of an absolute URL on a page with an opaque origin', () => {
vi.stubGlobal('location', { origin: 'null', href: 'about:srcdoc' });

expect(trackStreamedFetch('stream', 'https://api.example.com/v1/chat')).toHaveBeenCalledWith(
expect.objectContaining({
name: 'GET api.example.com',
attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }),
}),
);
});
Expand All @@ -66,7 +78,7 @@ describe('fetchStreamPerformanceIntegration', () => {
expect(trackStreamedFetch('stream', 'data:text/event-stream,data: hi')).toHaveBeenCalledWith(
expect.objectContaining({
name: 'GET',
attributes: expect.objectContaining({ 'url.domain': undefined }),
attributes: expect.objectContaining({ [URL_DOMAIN]: undefined }),
}),
);
});
Expand All @@ -75,7 +87,7 @@ describe('fetchStreamPerformanceIntegration', () => {
expect(trackStreamedFetch('static', 'https://api.example.com/v1/chat?stream=1')).toHaveBeenCalledWith(
expect.objectContaining({
name: 'GET https://api.example.com/v1/chat',
attributes: expect.objectContaining({ 'url.domain': 'api.example.com' }),
attributes: expect.objectContaining({ [URL_DOMAIN]: 'api.example.com' }),
}),
);
});
Expand Down
103 changes: 93 additions & 10 deletions packages/browser/test/tracing/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Client } from '@sentry/core';
import * as utils from '@sentry/core';
import * as coreBrowser from '@sentry/core/browser';
import * as browserUtils from '@sentry/browser-utils';
import { HTTP_REQUEST_METHOD } from '@sentry/conventions/attributes';
import { HTTP_REQUEST_METHOD, SERVER_ADDRESS, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes';
import type { MockInstance } from 'vitest';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { BrowserClient } from '../../src/client';
Expand Down Expand Up @@ -223,12 +223,12 @@ describe('instrumentOutgoingRequests', () => {

const requestSpanJson = utils.spanToJSON(requestSpan!);
expect(requestSpanJson.name).toBe('GET example.com');
expect(requestSpanJson.attributes['url.domain']).toBe('example.com');
expect(requestSpanJson.attributes['server.address']).toBe('example.com:8443');
expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('example.com');
expect(requestSpanJson.attributes[SERVER_ADDRESS]).toBe('example.com:8443');
});

it('resolves a relative fetch URL against the page origin for the streamed span name', () => {
vi.stubGlobal('location', { origin: 'https://app.example.com' });
it('resolves a relative fetch URL against the page URL for the streamed span name', () => {
vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' });

let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined;
let requestSpan: utils.Span | undefined;
Expand All @@ -255,13 +255,13 @@ describe('instrumentOutgoingRequests', () => {

const requestSpanJson = utils.spanToJSON(requestSpan!);
expect(requestSpanJson.name).toBe('GET app.example.com');
expect(requestSpanJson.attributes['url.domain']).toBe('app.example.com');
expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('app.example.com');

vi.unstubAllGlobals();
});

it('resolves a relative XHR URL against the page origin for the streamed span name', () => {
vi.stubGlobal('location', { origin: 'https://app.example.com' });
it('resolves a relative XHR URL against the page URL for the streamed span name', () => {
vi.stubGlobal('location', { origin: 'https://app.example.com', href: 'https://app.example.com/' });

let xhrHandler: ((data: utils.HandlerDataXhr) => void) | undefined;
let requestSpan: utils.Span | undefined;
Expand Down Expand Up @@ -294,11 +294,94 @@ describe('instrumentOutgoingRequests', () => {

const requestSpanJson = utils.spanToJSON(requestSpan!);
expect(requestSpanJson.name).toBe('GET app.example.com');
expect(requestSpanJson.attributes['url.domain']).toBe('app.example.com');
expect(requestSpanJson.attributes[URL_DOMAIN]).toBe('app.example.com');

vi.unstubAllGlobals();
});

describe('resolving request URLs against location.href', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it.each([
{
name: 'an absolute URL on a page with an opaque origin',
location: { origin: 'null', href: 'about:srcdoc' },
requestUrl: 'https://api.example.org/v1/users',
expected: 'https://api.example.org/v1/users',
},
{
name: 'a relative path without a leading slash',
location: { origin: 'https://app.example.com', href: 'https://app.example.com/dashboard/settings' },
requestUrl: 'users',
expected: 'https://app.example.com/dashboard/users',
},
])('sets url.full on XHR spans for $name', ({ location, requestUrl, expected }) => {
vi.stubGlobal('location', location);

let xhrHandler: ((data: utils.HandlerDataXhr) => void) | undefined;
let requestSpan: utils.Span | undefined;

vi.spyOn(browserUtils, 'addXhrInstrumentationHandler').mockImplementation(handler => {
xhrHandler = handler;
});
const tracingClient = new BrowserClient(getDefaultBrowserClientOptions({ tracesSampleRate: 1 }));
utils.setCurrentClient(tracingClient);
utils._INTERNAL_setSpanForScope(utils.getCurrentScope(), new utils.SentrySpan({ sampled: true }));

instrumentOutgoingRequests(tracingClient, {
traceFetch: false,
enableHTTPTimings: false,
onRequestSpanStart: span => {
requestSpan = span;
},
});
xhrHandler?.({
xhr: {
[browserUtils.SENTRY_XHR_DATA_KEY]: {
method: 'GET',
url: requestUrl,
request_headers: {},
},
setRequestHeader: vi.fn(),
},
startTimestamp: Date.now(),
} as utils.HandlerDataXhr);

expect(utils.spanToJSON(requestSpan!).attributes[URL_FULL]).toBe(expected);
});

it('sets url.domain on fetch spans for an absolute URL on a page with an opaque origin', () => {
vi.stubGlobal('location', { origin: 'null', href: 'about:srcdoc' });

let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined;
let requestSpan: utils.Span | undefined;

vi.spyOn(utils, 'addFetchInstrumentationHandler').mockImplementation(handler => {
fetchHandler = handler;
});
const tracingClient = new BrowserClient(getDefaultBrowserClientOptions({ tracesSampleRate: 1 }));
utils.setCurrentClient(tracingClient);
utils._INTERNAL_setSpanForScope(utils.getCurrentScope(), new utils.SentrySpan({ sampled: true }));

instrumentOutgoingRequests(tracingClient, {
traceXHR: false,
enableHTTPTimings: false,
onRequestSpanStart: span => {
requestSpan = span;
},
});
fetchHandler?.({
fetchData: { method: 'GET', url: 'https://api.example.org/v1/users' },
args: ['https://api.example.org/v1/users'],
startTimestamp: Date.now(),
});

expect(utils.spanToJSON(requestSpan!).attributes[URL_DOMAIN]).toBe('api.example.org');
});
});

it('falls back to the request method for a data URL, which has no domain', () => {
let fetchHandler: ((data: utils.HandlerDataFetch) => void) | undefined;
let requestSpan: utils.Span | undefined;
Expand All @@ -325,7 +408,7 @@ describe('instrumentOutgoingRequests', () => {

const requestSpanJson = utils.spanToJSON(requestSpan!);
expect(requestSpanJson.name).toBe('GET');
expect(requestSpanJson.attributes['url.domain']).toBeUndefined();
expect(requestSpanJson.attributes[URL_DOMAIN]).toBeUndefined();
});

describe('XHR trace header span', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ interface InstrumentFetchRequestOptions {
spanOrigin?: SpanOrigin;
propagateTraceparent?: boolean;
onRequestSpanEnd?: (span: Span, responseInformation: ResponseHookInfo) => void;
/** Base URL for relative request URLs. Browsers pass the page origin; server runtimes have none. */
/** Base URL for relative request URLs. Browsers pass the page URL; server runtimes have none. */
urlBase?: string;
}

Expand Down
Loading