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.

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.

Confirmed. Need to update the following as well to match:

  • packages/browser/src/tracing/request.ts 402-403
  • packages/node/src/utils/outgoingFetchRequest.ts 261-264

Also found these:

  • packages/astro/src/server/middleware.ts 236 and 240
  • packages/bun/src/integrations/bunserver.ts 277 and 280

Also, there are TODO(v11) comments about this in packages/opentelemetry/src/utils/parseSpanDescription.ts line 202 and packages/opentelemetry/src/spanExporter.ts line 456 that seem related, and I think can be dropped, since the todo will be to-done after this PR.

Might want to verify that the stripping happening in the otel lib isn't going to double-strip these fields, since the field set in packages/core/src/integrations/requestdata.ts is stripped already. I think it's actually behaving properly, stripping it only where it needs to and not anywhere else (apart from the handful of locations noted above), and it'd likely fail a bunch of tests if it was double-stripping. In which case the TODO(v11) comments are definitely a bit of a hazard.

}
}
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 } : {}),
Comment on lines +27 to +28

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.

These get serialized with JSON.stringify, so undefined values are safe. Not a blocker, but worth cleaning up where we encounter it, imo. Also, these can probably pull in the costs to cut down on bundle size ever so slightly:

Suggested change
...(parsedUrl.search ? { 'http.query': parsedUrl.search.slice(1) || undefined } : {}),
...(parsedUrl.hash ? { 'http.fragment': parsedUrl.hash.slice(1) || undefined } : {}),
[HTTP_FRAGMENT]: parsedUrl.hash?.slice(1) || undefined,
[HTTP_QUERY]: parsedUrl?.search?.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;
Comment on lines +173 to +174

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.

Same here

Suggested change
const query = urlObj?.search ? urlObj.search.slice(1) || undefined : undefined;
const fragment = urlObj?.hash ? urlObj.hash.slice(1) || undefined : undefined;
const query = urlObj?.search?.slice(1) || undefined;
const fragment = urlObj?.hash?.slice(1) || 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