diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts index e2b3e346a214..1bf8c12f5494 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts @@ -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', @@ -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', @@ -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', @@ -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', diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts index cd900f6b2a43..e6cc208ed0b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/test.ts @@ -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:.+$/), diff --git a/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts index e79589dcac76..9d81926b5a8b 100644 --- a/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-streamed/tests/spans.test.ts @@ -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$/), @@ -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' }, }, diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 4e81bd254258..7a9a4f08e14f 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -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:', diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 7887de49c687..e883af841cf8 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -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'; @@ -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', }; @@ -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; } } return attributes; diff --git a/packages/core/src/integrations/http/add-outgoing-request-breadcrumb.ts b/packages/core/src/integrations/http/add-outgoing-request-breadcrumb.ts index 251dbfc540a6..5e6ae1047c89 100644 --- a/packages/core/src/integrations/http/add-outgoing-request-breadcrumb.ts +++ b/packages/core/src/integrations/http/add-outgoing-request-breadcrumb.ts @@ -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, diff --git a/packages/core/src/utils/url.ts b/packages/core/src/utils/url.ts index 82d85b5bdc1f..d522ba9e4114 100644 --- a/packages/core/src/utils/url.ts +++ b/packages/core/src/utils/url.ts @@ -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, @@ -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'; } @@ -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'; } @@ -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; } } } diff --git a/packages/core/test/lib/fetch.test.ts b/packages/core/test/lib/fetch.test.ts index 5e949439c739..a169dd01d31c 100644 --- a/packages/core/test/lib/fetch.test.ts +++ b/packages/core/test/lib/fetch.test.ts @@ -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', }, }); }); diff --git a/packages/core/test/lib/integrations/http/add-outgoing-request-breadcrumb.test.ts b/packages/core/test/lib/integrations/http/add-outgoing-request-breadcrumb.test.ts index 8ed6a1f3d660..770ca5008369 100644 --- a/packages/core/test/lib/integrations/http/add-outgoing-request-breadcrumb.test.ts +++ b/packages/core/test/lib/integrations/http/add-outgoing-request-breadcrumb.test.ts @@ -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(), ); diff --git a/packages/core/test/lib/utils/url.test.ts b/packages/core/test/lib/utils/url.test.ts index 7bdfcfd63804..17948ce1b643 100644 --- a/packages/core/test/lib/utils/url.test.ts +++ b/packages/core/test/lib/utils/url.test.ts @@ -415,6 +415,7 @@ describe('getHttpSpanDetailsFromUrlObject', () => { 'sentry.origin': 'test-origin', 'sentry.source': 'url', 'url.path': '/api/users', + url: '/api/users', }); }); @@ -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:', }); @@ -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', @@ -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', @@ -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:', }); @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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:', }); @@ -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', @@ -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', @@ -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:', }); diff --git a/packages/node/src/integrations/http/httpServerSpansIntegration.ts b/packages/node/src/integrations/http/httpServerSpansIntegration.ts index a09a1759fd84..2e793c3d14fb 100644 --- a/packages/node/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node/src/integrations/http/httpServerSpansIntegration.ts @@ -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, @@ -51,6 +53,7 @@ import { bindScopeToEmitter, startInactiveSpan, withActiveSpan, + getSanitizedUrlStringFromUrlObject, SPAN_KIND, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; @@ -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']; @@ -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, @@ -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, diff --git a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts index 89a754b36274..ebdcf9108b6e 100644 --- a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts +++ b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts @@ -215,6 +215,7 @@ function onRequestCreated(config: NodeFetchOptions, { request }: RequestMessage) // Skip instrumenting this request. return; } + const urlScheme = requestUrl.protocol.replace(':', ''); const requestMethod = getRequestMethod(request.method); const attributes: SpanAttributes = {