Skip to content
Open
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 @@ -32,7 +32,7 @@ sentryTest('strips query params in fetch request spans', async ({ getLocalTestUr
'http.method': 'GET',
'http.url': 'http://sentry-test-site.example/0?id=123;page=5',
'url.full': 'http://sentry-test-site.example/0?id=123;page=5',
'http.query': '?id=123;page=5',
'http.query': 'id=123;page=5',
'http.response.status_code': 200,
'http.response_content_length': 2,
'sentry.op': 'http.client',
Expand Down Expand Up @@ -76,7 +76,7 @@ sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestU
'http.method': 'GET',
'http.url': 'http://sentry-test-site.example/1#fragment',
'url.full': 'http://sentry-test-site.example/1#fragment',
'http.fragment': '#fragment',
'http.fragment': 'fragment',
'http.response.status_code': 200,
'http.response_content_length': 2,
'sentry.op': 'http.client',
Expand Down Expand Up @@ -120,8 +120,8 @@ sentryTest('strips hash fragment and query params in fetch request spans', async
'http.method': 'GET',
'http.url': 'http://sentry-test-site.example/2?id=1#fragment',
'url.full': 'http://sentry-test-site.example/2?id=1#fragment',
'http.query': '?id=1',
'http.fragment': '#fragment',
'http.query': 'id=1',
'http.fragment': 'fragment',
'http.response.status_code': 200,
'http.response_content_length': 2,
'sentry.op': 'http.client',
Expand Down Expand Up @@ -165,8 +165,8 @@ sentryTest(
'http.method': 'GET',
'http.url': 'http://sentry-test.io/api/users?id=1#fragment',
'url.full': 'http://sentry-test.io/api/users?id=1#fragment',
'http.query': '?id=1',
'http.fragment': '#fragment',
'http.query': 'id=1',
'http.fragment': 'fragment',
'http.response.status_code': 200,
'http.response_content_length': 2,
'sentry.op': 'http.client',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ it('sends a streamed span envelope with correct spans for a manually started spa
type: 'string',
value: 'localhost',
},
url: {
type: 'string',
value: expect.stringMatching(/^http:\/\/localhost:.+$/),
},
'url.full': {
type: 'string',
value: expect.stringMatching(/^http:\/\/localhost:.+$/),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ const SEGMENT_SPAN = {
type: 'string',
value: expect.any(String),
},
url: {
type: 'string',
value: expect.stringMatching(/^http:\/\/localhost:\d+\/test-sentry-span$/),
},
'url.full': {
type: 'string',
value: expect.stringMatching(/^http:\/\/localhost:\d+\/test-sentry-span$/),
Expand Down Expand Up @@ -234,6 +238,7 @@ test('OTel span appears as child of Sentry span (interop)', async ({ baseURL })
attributes: {
...SEGMENT_SPAN.attributes,
'sentry.segment.name': { type: 'string', value: 'GET /test-interop' },
url: { type: 'string', value: expect.stringMatching(/^http:\/\/localhost:\d+\/test-interop$/) },
'url.full': { type: 'string', value: expect.stringMatching(/^http:\/\/localhost:\d+\/test-interop$/) },
'url.path': { type: 'string', value: '/test-interop' },
},
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ describe('withSentry', () => {
'sentry.source': 'route',
'http.request.method': 'GET',
'url.full': 'https://example.com/',
url: 'https://example.com/',
'server.address': 'example.com',
'network.protocol.name': 'HTTP/1.1',
'url.scheme': 'https:',
Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes';
/* eslint-disable max-lines */
import {
HTTP_FRAGMENT,
HTTP_METHOD,
HTTP_QUERY,
HTTP_URL,
SERVER_ADDRESS,
URL_FULL,
} from '@sentry/conventions/attributes';
import { getClient } from './currentScopes';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes';
import { setHttpStatus, SPAN_STATUS_ERROR, spanIsIgnored, startInactiveSpan } from './tracing';
Expand Down Expand Up @@ -387,7 +395,8 @@ function getFetchSpanAttributes(
const attributes: SpanAttributes = {
url: stripDataUrlContent(url),
type: 'fetch',
'http.method': method,
// oxlint-disable-next-line typescript/no-deprecated
[HTTP_METHOD]: method,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
};
Expand All @@ -396,13 +405,13 @@ function getFetchSpanAttributes(
// oxlint-disable-next-line typescript/no-deprecated
attributes[HTTP_URL] = stripDataUrlContent(parsedUrl.href);
attributes[URL_FULL] = stripDataUrlContent(parsedUrl.href);
attributes['server.address'] = parsedUrl.host;
attributes[SERVER_ADDRESS] = parsedUrl.host;
}
if (parsedUrl.search) {
attributes['http.query'] = parsedUrl.search;
attributes[HTTP_QUERY] = parsedUrl.search.slice(1) || undefined;
}
if (parsedUrl.hash) {
attributes['http.fragment'] = parsedUrl.hash;
attributes[HTTP_FRAGMENT] = parsedUrl.hash.slice(1) || undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Split HTTP query fragment formats

Medium Severity

This commit normalizes http.query and http.fragment by stripping leading ? and # in core fetch, HTTP breadcrumbs, URL span helpers, and Node server spans, but browser XHR and Node undici fetch breadcrumbs still emit the raw URL.search / URL.hash values with those prefixes. The same app can therefore report different query and fragment shapes on http.client spans and HTTP breadcrumbs depending on the API used.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d6bc9b0. Configure here.

}
}
return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function addOutgoingRequestBreadcrumb(
status_code: statusCode,
url: getSanitizedUrlString(parsedUrl),
'http.method': request.method || 'GET',
...(parsedUrl.search ? { 'http.query': parsedUrl.search } : {}),
...(parsedUrl.hash ? { 'http.fragment': parsedUrl.hash } : {}),
...(parsedUrl.search ? { 'http.query': parsedUrl.search.slice(1) || undefined } : {}),
...(parsedUrl.hash ? { 'http.fragment': parsedUrl.hash.slice(1) || undefined } : {}),
},
type: 'http',
level,
Expand Down
37 changes: 29 additions & 8 deletions packages/core/src/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { URL_FULL } from '@sentry/conventions/attributes';
import {
HTTP_FRAGMENT,
HTTP_QUERY,
HTTP_ROUTE,
SERVER_ADDRESS,
URL_DOMAIN,
URL_FRAGMENT,
URL_FULL,
URL_PATH,
URL_PORT,
URL_QUERY,
URL_SCHEME,
URL_TEMPLATE,
} from '@sentry/conventions/attributes';
import {
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
Expand Down Expand Up @@ -165,7 +178,7 @@ export function getHttpSpanDetailsFromUrlObject(

if (routeName) {
// This is based on https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name
attributes[kind === 'server' ? 'http.route' : 'url.template'] = routeName;
attributes[kind === 'server' ? HTTP_ROUTE : URL_TEMPLATE] = routeName;
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route';
}

Expand All @@ -174,14 +187,22 @@ export function getHttpSpanDetailsFromUrlObject(
}

if (urlObject) {
attributes['url'] = getSanitizedUrlStringFromUrlObject(urlObject);

if (urlObject.search) {
attributes['url.query'] = urlObject.search;
const query = urlObject.search.slice(1) || undefined;
attributes[URL_QUERY] = query;
// legacy attribute
attributes[HTTP_QUERY] = query;
}
if (urlObject.hash) {
attributes['url.fragment'] = urlObject.hash;
const fragment = urlObject.hash.slice(1) || undefined;
attributes[URL_FRAGMENT] = fragment;
// legacy attribute
attributes[HTTP_FRAGMENT] = fragment;
}
if (urlObject.pathname) {
attributes['url.path'] = urlObject.pathname;
attributes[URL_PATH] = urlObject.pathname;
if (urlObject.pathname === '/') {
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route';
}
Expand All @@ -190,13 +211,13 @@ export function getHttpSpanDetailsFromUrlObject(
if (!isURLObjectRelative(urlObject)) {
attributes[URL_FULL] = urlObject.href;
if (urlObject.port) {
attributes['url.port'] = urlObject.port;
attributes[URL_PORT] = urlObject.port;
}
if (urlObject.protocol) {
attributes['url.scheme'] = urlObject.protocol;
attributes[URL_SCHEME] = urlObject.protocol;
}
if (urlObject.hostname) {
attributes[kind === 'server' ? 'server.address' : 'url.domain'] = urlObject.hostname;
attributes[kind === 'server' ? SERVER_ADDRESS : URL_DOMAIN] = urlObject.hostname;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/lib/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,8 @@ describe('instrumentFetchRequest', () => {
'http.url': url,
[URL_FULL]: url,
'server.address': 'api.example.com',
'http.query': '?include=profile',
'http.fragment': '#bio',
'http.query': 'include=profile',
'http.fragment': 'bio',
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('addOutgoingRequestBreadcrumb', () => {

expect(breadcrumbsModule.addBreadcrumb).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ 'http.query': '?foo=bar' }),
data: expect.objectContaining({ 'http.query': 'foo=bar' }),
}),
expect.anything(),
);
Expand Down
26 changes: 22 additions & 4 deletions packages/core/test/lib/utils/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.origin': 'test-origin',
'sentry.source': 'url',
'url.path': '/api/users',
url: '/api/users',
});
});

Expand All @@ -426,9 +427,12 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.origin': 'test-origin',
'sentry.source': 'url',
'url.path': '/api/users',
'url.query': '?q=test',
'url.fragment': '#section',
'url.query': 'q=test',
'url.fragment': 'section',
'http.query': 'q=test',
'http.fragment': 'section',
'url.full': 'https://example.com/api/users?q=test#section',
url: 'https://example.com/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
});
Expand All @@ -443,6 +447,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://example.com/api/users',
url: 'https://example.com/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
'http.request.method': 'POST',
Expand All @@ -464,6 +469,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'route',
'url.path': '/api/users',
'url.full': 'https://example.com/api/users',
url: 'https://example.com/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
'http.route': '/api/users/:id',
Expand All @@ -479,6 +485,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'route',
'url.path': '/',
'url.full': 'https://example.com/',
url: 'https://example.com/',
'server.address': 'example.com',
'url.scheme': 'https:',
});
Expand All @@ -493,6 +500,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://example.com:8080/api/users',
url: 'https://example.com:8080/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
'url.port': '8080',
Expand All @@ -508,6 +516,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://example.com:3000/api/users',
url: 'https://example.com:3000/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
'url.port': '3000',
Expand All @@ -530,6 +539,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'route',
'url.path': '/api/users/123',
'url.full': 'https://example.com/api/users/123',
url: 'https://example.com/api/users/123',
'server.address': 'example.com',
'url.scheme': 'https:',
'http.route': '/api/users/:id',
Expand All @@ -551,8 +561,10 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.origin': 'test-origin',
'sentry.source': 'route',
'url.path': '/api/search',
'url.query': '?q=test&page=1',
'url.query': 'q=test&page=1',
'http.query': 'q=test&page=1',
'url.full': 'https://example.com/api/search?q=test&page=1',
url: 'https://example.com/api/search',
'server.address': 'example.com',
'url.scheme': 'https:',
'http.route': '/api/search',
Expand All @@ -573,8 +585,10 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.origin': 'test-origin',
'sentry.source': 'route',
'url.path': '/api/docs',
'url.fragment': '#section-1',
'url.fragment': 'section-1',
'http.fragment': 'section-1',
'url.full': 'https://example.com/api/docs#section-1',
url: 'https://example.com/api/docs',
'server.address': 'example.com',
'url.scheme': 'https:',
'http.route': '/api/docs',
Expand All @@ -590,6 +604,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://user:pass@example.com/api/users',
url: 'https://%filtered%:%filtered%@example.com/api/users',
'server.address': 'example.com',
'url.scheme': 'https:',
});
Expand All @@ -604,6 +619,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://192.168.1.1:8080/api/users',
url: 'https://192.168.1.1:8080/api/users',
'server.address': '192.168.1.1',
'url.scheme': 'https:',
'url.port': '8080',
Expand All @@ -619,6 +635,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/api/users',
'url.full': 'https://[2001:db8::1]:8080/api/users',
url: 'https://[2001:db8::1]:8080/api/users',
'server.address': '[2001:db8::1]',
'url.scheme': 'https:',
'url.port': '8080',
Expand All @@ -634,6 +651,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => {
'sentry.source': 'url',
'url.path': '/users',
'url.full': 'https://api.example.com/users',
url: 'https://api.example.com/users',
'server.address': 'api.example.com',
'url.scheme': 'https:',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { RPCType, setRPCMetadata } from '@opentelemetry/core';
import {
HTTP_CLIENT_IP,
HTTP_FLAVOR,
HTTP_FRAGMENT,
HTTP_HOST,
HTTP_METHOD,
HTTP_QUERY,
HTTP_RESPONSE_STATUS_CODE,
HTTP_ROUTE,
HTTP_SCHEME,
Expand Down Expand Up @@ -51,6 +53,7 @@ import {
bindScopeToEmitter,
startInactiveSpan,
withActiveSpan,
getSanitizedUrlStringFromUrlObject,
SPAN_KIND,
} from '@sentry/core';
import { DEBUG_BUILD } from '../../debug-build';
Expand Down Expand Up @@ -152,6 +155,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions

const fullUrl = normalizedRequest.url || request.url || '/';
const urlObj = parseStringToURLObject(fullUrl);
const sanitizedUrl = urlObj ? getSanitizedUrlStringFromUrlObject(urlObj) : undefined;

const headers = request.headers;
const userAgent = headers['user-agent'];
Expand All @@ -166,6 +170,9 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
const httpTargetWithoutQueryFragment = urlObj ? urlObj.pathname : stripUrlQueryAndFragment(fullUrl);
const bestEffortTransactionName = `${method} ${httpTargetWithoutQueryFragment}`;

const query = urlObj?.search ? urlObj.search.slice(1) || undefined : undefined;
const fragment = urlObj?.hash ? urlObj.hash.slice(1) || undefined : undefined;

const span = startInactiveSpan({
name: bestEffortTransactionName,
kind: SPAN_KIND.SERVER,
Expand All @@ -179,7 +186,10 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions
// Old Semantic Conventions attributes - added for compatibility with what `@opentelemetry/instrumentation-http` output before
/* eslint-disable typescript/no-deprecated */
[HTTP_URL]: fullUrl,
url: sanitizedUrl,
[HTTP_METHOD]: normalizedRequest.method,
[HTTP_QUERY]: query,
[HTTP_FRAGMENT]: fragment,
[HTTP_TARGET]: urlObj ? `${urlObj.pathname}${urlObj.search}` : httpTargetWithoutQueryFragment,
[HTTP_HOST]: host,
[NET_HOST_NAME]: hostname,
Expand Down
Loading
Loading