Skip to content

Commit

Permalink
feat(core): Add server.address to browser http.client spans (#11634)
Browse files Browse the repository at this point in the history
Closes #11632
  • Loading branch information
mydea authored Apr 17, 2024
1 parent 2c840c3 commit daf2edf
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ module.exports = [
'tls',
],
gzip: true,
limit: '150 KB',
limit: '160 KB',
},
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fetch('/test-req/0').then(
fetch('/test-req/1', { headers: { 'X-Test-Header': 'existing-header' } }).then(fetch('/test-req/2')),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from '@playwright/test';

import { TEST_HOST, sentryTest } from '../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipTracingTest,
waitForTransactionRequestOnUrl,
} from '../../../../utils/helpers';

sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });
const req = await waitForTransactionRequestOnUrl(page, url);
const tracingEvent = envelopeRequestParser(req);

const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(3);

requestSpans?.forEach((span, index) =>
expect(span).toMatchObject({
description: `GET /test-req/${index}`,
parent_span_id: tracingEvent.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: tracingEvent.contexts?.trace?.trace_id,
data: {
'http.method': 'GET',
'http.url': `${TEST_HOST}/test-req/${index}`,
url: `/test-req/${index}`,
'server.address': 'sentry-test.io',
type: 'fetch',
},
}),
);
});

sentryTest('should attach `sentry-trace` header to fetch requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

const request1 = requests[0];
const requestHeaders1 = request1.headers();
expect(requestHeaders1).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
});

const request2 = requests[1];
const requestHeaders2 = request2.headers();
expect(requestHeaders2).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
'x-test-header': 'existing-header',
});

const request3 = requests[2];
const requestHeaders3 = request3.headers();
expect(requestHeaders3).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestPath,
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: tracingEvent.contexts?.trace?.trace_id,
data: {
'http.method': 'GET',
'http.url': `http://example.com/${index}`,
url: `http://example.com/${index}`,
'server.address': 'example.com',
type: 'fetch',
},
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const xhr_1 = new XMLHttpRequest();
xhr_1.open('GET', '/test-req/0');
xhr_1.send();

const xhr_2 = new XMLHttpRequest();
xhr_2.open('GET', '/test-req/1');
xhr_2.setRequestHeader('X-Test-Header', 'existing-header');
xhr_2.send();

const xhr_3 = new XMLHttpRequest();
xhr_3.open('GET', '/test-req/2');
xhr_3.send();
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from '@playwright/test';

import { TEST_HOST, sentryTest } from '../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipTracingTest,
waitForTransactionRequestOnUrl,
} from '../../../../utils/helpers';

sentryTest('should create spans for xhr requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });
const req = await waitForTransactionRequestOnUrl(page, url);
const tracingEvent = envelopeRequestParser(req);

const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client');

expect(requestSpans).toHaveLength(3);

requestSpans?.forEach((span, index) =>
expect(span).toMatchObject({
description: `GET /test-req/${index}`,
parent_span_id: tracingEvent.contexts?.trace?.span_id,
span_id: expect.any(String),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: tracingEvent.contexts?.trace?.trace_id,
data: {
'http.method': 'GET',
'http.url': `${TEST_HOST}/test-req/${index}`,
url: `/test-req/${index}`,
'server.address': 'sentry-test.io',
type: 'xhr',
},
}),
);
});

sentryTest('should attach `sentry-trace` header to xhr requests', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

const request1 = requests[0];
const requestHeaders1 = request1.headers();
expect(requestHeaders1).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
});

const request2 = requests[1];
const requestHeaders2 = request2.headers();
expect(requestHeaders2).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
'x-test-header': 'existing-header',
});

const request3 = requests[2];
const requestHeaders3 = request3.headers();
expect(requestHeaders3).toMatchObject({
'sentry-trace': expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/),
baggage: expect.any(String),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ sentryTest('should create spans for XHR requests', async ({ getLocalTestPath, pa
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: eventData.contexts?.trace?.trace_id,
data: {
'http.method': 'GET',
'http.url': `http://example.com/${index}`,
url: `http://example.com/${index}`,
'server.address': 'example.com',
type: 'xhr',
},
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru
'http.response.status_code': 200,
type: 'fetch',
url: 'http://localhost:3030/',
'http.url': 'http://localhost:3030/',
'server.address': 'localhost:3030',
'sentry.op': 'http.client',
'sentry.origin': 'auto.http.wintercg_fetch',
},
Expand Down
32 changes: 31 additions & 1 deletion packages/browser/src/tracing/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
addXhrInstrumentationHandler,
} from '@sentry-internal/browser-utils';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SentryNonRecordingSpan,
getActiveSpan,
Expand All @@ -26,6 +27,7 @@ import {
browserPerformanceTimeOrigin,
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
parseUrl,
stringMatchesSomePattern,
} from '@sentry/utils';
import { WINDOW } from '../helpers';
Expand Down Expand Up @@ -115,6 +117,18 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
if (traceFetch) {
addFetchInstrumentationHandler(handlerData => {
const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
// 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;
createdSpan.setAttributes({
'http.url': fullUrl,
'server.address': host,
});
}

if (enableHTTPTimings && createdSpan) {
addHTTPTimings(createdSpan);
}
Expand Down Expand Up @@ -310,17 +324,22 @@ export function xhrCallback(

const hasParent = !!getActiveSpan();

const fullUrl = getFullURL(sentryXhrData.url);
const host = fullUrl ? parseUrl(fullUrl).host : undefined;

const span =
shouldCreateSpanResult && hasParent
? startInactiveSpan({
name: `${sentryXhrData.method} ${sentryXhrData.url}`,
attributes: {
type: 'xhr',
'http.method': sentryXhrData.method,
'http.url': fullUrl,
url: sentryXhrData.url,
'server.address': host,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
},
op: 'http.client',
})
: new SentryNonRecordingSpan();

Expand Down Expand Up @@ -381,3 +400,14 @@ 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;
}
}
Loading

0 comments on commit daf2edf

Please sign in to comment.