diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts index a5bbc408862c..548b709fdbcf 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts @@ -242,6 +242,8 @@ test.describe('nested SSR routes (client, server, server request)', () => { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.fetch', url: expect.stringContaining('/api/user/myUsername123.json'), + 'http.url': 'http://localhost:3030/api/user/myUsername123.json', + 'url.full': 'http://localhost:3030/api/user/myUsername123.json', }, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts index 03539a781ec1..905cdcdbdba7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts @@ -75,6 +75,7 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru type: 'fetch', url: 'http://localhost:3030/', 'http.url': 'http://localhost:3030/', + 'url.full': 'http://localhost:3030/', 'server.address': 'localhost:3030', 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.wintercg_fetch', diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 60dbe88f2fa7..4f6f48f00cd2 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -8,7 +8,6 @@ import { isPrimitive, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_URL_FULL, setMeasurement, spanToJSON, stringMatchesSomePattern, @@ -30,6 +29,7 @@ import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; import { DEBUG_BUILD } from '../debug-build'; +import { URL_FULL } from '@sentry/conventions/attributes'; interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; } @@ -775,7 +775,7 @@ export function _addResourceSpans( attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin); - attributes[SEMANTIC_ATTRIBUTE_URL_FULL] = resourceUrl; + attributes[URL_FULL] = resourceUrl; _setResourceRequestAttributes(entry, attributes, [ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus diff --git a/packages/browser/src/integrations/graphqlClient.ts b/packages/browser/src/integrations/graphqlClient.ts index f2d298027f3d..71c05d8381a3 100644 --- a/packages/browser/src/integrations/graphqlClient.ts +++ b/packages/browser/src/integrations/graphqlClient.ts @@ -5,13 +5,12 @@ import { isString, SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_URL_FULL, spanToJSON, stringMatchesSomePattern, } from '@sentry/core/browser'; import type { FetchHint, XhrHint } from '@sentry/browser-utils'; import { getBodyString, getFetchRequestArgBody, SENTRY_XHR_DATA_KEY } from '@sentry/browser-utils'; -import { GRAPHQL_DOCUMENT } from '@sentry/conventions/attributes'; +import { GRAPHQL_DOCUMENT, URL_FULL } from '@sentry/conventions/attributes'; interface GraphQLClientOptions { endpoints: Array; @@ -71,7 +70,7 @@ function _updateSpanWithGraphQLData(client: Client, options: GraphQLClientOption // Fall back to `url` because fetch instrumentation only sets `http.url` for absolute URLs; // relative URLs end up only in `url` (see `getFetchSpanAttributes` in packages/core/src/fetch.ts). - const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes['http.url'] || spanAttributes['url']; + const httpUrl = spanAttributes[URL_FULL] || spanAttributes['http.url'] || spanAttributes['url']; const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes['http.method']; if (!isString(httpUrl) || !isString(httpMethod)) { diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index c013a5939d7b..92234b2e9631 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,5 +1,6 @@ import { defineIntegration, safeSetSpanJSONAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core/browser'; import { getHttpRequestData, WINDOW } from '../helpers'; +import { URL_FULL } from '@sentry/conventions/attributes'; /** * Collects information about HTTP request headers and @@ -39,7 +40,7 @@ export const httpContextIntegration = defineIntegration(() => { safeSetSpanJSONAttributes(span, { // Coerce empty string to undefined so the helper's nullish check drops it, // rather than writing an empty `url.full` attribute onto the span. - 'url.full': spanOp !== 'http.client' ? reqData.url : undefined, + [URL_FULL]: spanOp !== 'http.client' ? reqData.url : undefined, 'http.request.header.user_agent': reqData.headers['User-Agent'], 'http.request.header.referer': reqData.headers['Referer'], }); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 38cb73d085cc..1c33ceb1f8af 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -40,6 +40,7 @@ import { } from '@sentry/browser-utils'; import type { BrowserClient } from '../client'; import { baggageHeaderHasSentryValues, createHeadersSafely, getFullURL, isPerformanceResourceTiming } from './utils'; +import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes'; /** Options for Request Instrumentation */ export interface RequestInstrumentationOptions { @@ -172,10 +173,11 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { extensions: {}, }; - test('enriches http.client span for absolute URLs (http.url attribute)', () => { + test('enriches http.client span for absolute URLs', () => { const handler = setupHandler([/\/graphql$/]); const span = new SentrySpan({ name: 'POST http://localhost:4000/graphql', @@ -360,6 +361,7 @@ describe('GraphqlClient', () => { attributes: { 'http.method': 'POST', 'http.url': 'http://localhost:4000/graphql', + [URL_FULL]: 'http://localhost:4000/graphql', url: 'http://localhost:4000/graphql', }, }); @@ -371,9 +373,27 @@ describe('GraphqlClient', () => { expect(json.data['graphql.document']).toBe(requestBody.query); }); + test('enriches http.client span when only url.full is present', () => { + const handler = setupHandler([/\/graphql$/]); + const span = new SentrySpan({ + name: 'POST http://localhost:4000/graphql', + op: 'http.client', + attributes: { + 'http.method': 'POST', + [URL_FULL]: 'http://localhost:4000/graphql', + }, + }); + + handler(span, makeFetchHint('http://localhost:4000/graphql', requestBody)); + + const json = spanToJSON(span); + expect(json.description).toBe('POST http://localhost:4000/graphql (query GetHello)'); + expect(json.data['graphql.document']).toBe(requestBody.query); + }); + test('enriches http.client span for relative URLs (only url attribute)', () => { const handler = setupHandler([/\/graphql$/]); - // Fetch instrumentation does NOT set http.url for relative URLs — only `url`. + // Fetch instrumentation does not set `http.url` or `url.full` for relative URLs. const span = new SentrySpan({ name: 'POST /graphql', op: 'http.client', @@ -433,6 +453,7 @@ describe('GraphqlClient', () => { attributes: { 'http.method': 'POST', 'http.url': 'http://localhost:4000/graphql', + [URL_FULL]: 'http://localhost:4000/graphql', url: 'http://localhost:4000/graphql', }, }); diff --git a/packages/bun/package.json b/packages/bun/package.json index df7c6404f38c..ec57e8cea1a8 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -51,6 +51,7 @@ "dependencies": { "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.1", "@sentry/core": "10.67.0", + "@sentry/conventions": "^0.16.0", "@sentry/node": "10.67.0", "@sentry/server-utils": "10.67.0" }, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 4a67ba93c029..6ff72e9eb754 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -15,6 +15,7 @@ import { withIsolationScope, } from '@sentry/core'; import type { ServeOptions } from 'bun'; +import { URL_FULL } from '@sentry/conventions/attributes'; const INTEGRATION_NAME = 'BunServer' as const; @@ -282,7 +283,7 @@ function getSpanAttributesFromParsedUrl( attributes['url.path'] = parsedUrl.pathname; } if (!isURLObjectRelative(parsedUrl)) { - attributes['url.full'] = parsedUrl.href; + attributes[URL_FULL] = parsedUrl.href; if (parsedUrl.port) { attributes['url.port'] = parsedUrl.port; } diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index bfe78d499260..3ce7a2575039 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,3 +1,4 @@ +import { HTTP_URL, 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'; @@ -388,7 +389,9 @@ function getFetchSpanAttributes( }; if (parsedUrl) { if (!isURLObjectRelative(parsedUrl)) { - attributes['http.url'] = stripDataUrlContent(parsedUrl.href); + // oxlint-disable-next-line typescript/no-deprecated + attributes[HTTP_URL] = stripDataUrlContent(parsedUrl.href); + attributes[URL_FULL] = stripDataUrlContent(parsedUrl.href); attributes['server.address'] = parsedUrl.host; } if (parsedUrl.search) { diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 27990ad2d6cc..8c4f0690073f 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -11,6 +11,7 @@ import { parseCookie } from '../utils/cookie'; import { httpHeadersToSpanAttributes } from '../utils/request'; import { getClientIPAddress, ipHeaderNames } from '../vendor/getIpAddress'; import { safeSetSpanJSONAttributes } from '../tracing/spans/captureSpan'; +import { URL_FULL, URL_QUERY } from '@sentry/conventions/attributes'; interface RequestDataIncludeOptions { cookies?: boolean; @@ -137,7 +138,7 @@ function addNormalizedRequestDataToSpan( const attributes: Record = {}; if (requestData.url) { - attributes['url.full'] = requestData.url; + attributes[URL_FULL] = requestData.url; } if (requestData.method) { @@ -145,7 +146,7 @@ function addNormalizedRequestDataToSpan( } if (requestData.query_string) { - attributes['url.query'] = normalizeQueryString(requestData.query_string); + attributes[URL_QUERY] = normalizeQueryString(requestData.query_string); } safeSetSpanJSONAttributes(span, attributes); diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 46f37fa902c2..62a8c10072aa 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -110,6 +110,9 @@ export const SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE = 'cache.item_size'; /** TODO: Remove these once we update to latest semantic conventions */ export const SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD = 'http.request.method'; +/** + * @deprecated Use `URL_FULL` `@sentry/conventions/attributes` instead. + */ export const SEMANTIC_ATTRIBUTE_URL_FULL = 'url.full'; /** diff --git a/packages/core/src/utils/url.ts b/packages/core/src/utils/url.ts index ad56bb846b9b..82d85b5bdc1f 100644 --- a/packages/core/src/utils/url.ts +++ b/packages/core/src/utils/url.ts @@ -1,8 +1,8 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; import { SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SEMANTIC_ATTRIBUTE_URL_FULL, } from '../semanticAttributes'; import type { SpanAttributes } from '../types/span'; @@ -188,7 +188,7 @@ export function getHttpSpanDetailsFromUrlObject( } if (!isURLObjectRelative(urlObject)) { - attributes[SEMANTIC_ATTRIBUTE_URL_FULL] = urlObject.href; + attributes[URL_FULL] = urlObject.href; if (urlObject.port) { attributes['url.port'] = urlObject.port; } diff --git a/packages/core/test/lib/fetch.test.ts b/packages/core/test/lib/fetch.test.ts index 6cfdb74e8a19..ff07b6b7e8e6 100644 --- a/packages/core/test/lib/fetch.test.ts +++ b/packages/core/test/lib/fetch.test.ts @@ -1,3 +1,4 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { HandlerDataFetch } from '../../src'; import { _INTERNAL_getTracingHeadersForFetchRequest, instrumentFetchRequest } from '../../src/fetch'; @@ -444,6 +445,49 @@ describe('_INTERNAL_getTracingHeadersForFetchRequest', () => { }); describe('instrumentFetchRequest', () => { + describe('span attributes', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('sets url.full for absolute URLs', () => { + const url = 'https://api.example.com/users/42?include=profile#bio'; + const activeSpan = new SentryNonRecordingSpan(); + const fetchSpan = new SentryNonRecordingSpan(); + hasSpansEnabled.mockReturnValue(true); + vi.spyOn(spanUtils, 'getActiveSpan').mockReturnValue(activeSpan); + const startInactiveSpanSpy = vi.spyOn(tracing, 'startInactiveSpan').mockReturnValue(fetchSpan); + + instrumentFetchRequest( + { + fetchData: { url, method: 'GET' }, + args: [url], + startTimestamp: Date.now(), + }, + () => true, + () => false, + {}, + { spanOrigin: 'auto.http.fetch' }, + ); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith({ + name: 'GET https://api.example.com/users/42', + attributes: { + url, + type: 'fetch', + 'http.method': 'GET', + 'sentry.origin': 'auto.http.fetch', + 'sentry.op': 'http.client', + 'http.url': url, + [URL_FULL]: url, + 'server.address': 'api.example.com', + 'http.query': '?include=profile', + 'http.fragment': '#bio', + }, + }); + }); + }); + describe('trace header span', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/ember/addon/utils/instrumentEmberAppInstanceForPerformance.ts b/packages/ember/addon/utils/instrumentEmberAppInstanceForPerformance.ts index 750f0115ee1a..826912a46c22 100644 --- a/packages/ember/addon/utils/instrumentEmberAppInstanceForPerformance.ts +++ b/packages/ember/addon/utils/instrumentEmberAppInstanceForPerformance.ts @@ -14,10 +14,7 @@ import { import type { Client, Span } from '@sentry/core'; import type { EmberRouterMain } from '../types'; import { getBackburner } from './performance'; - -const URL_FULL = 'url.full'; -const URL_PATH = 'url.path'; -const URL_TEMPLATE = 'url.template'; +import { URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; type TransitionWithIntent = Transition & { intent?: { url?: string } }; diff --git a/packages/ember/package.json b/packages/ember/package.json index 33be748434ad..a31376cba1b5 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -34,6 +34,7 @@ "@embroider/macros": "^1.16.0", "@sentry/browser": "10.67.0", "@sentry/core": "10.67.0", + "@sentry/conventions": "^0.16.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/nextjs/src/server/vercelQueuesMonitoring.ts b/packages/nextjs/src/server/vercelQueuesMonitoring.ts index cfe367c46470..2f759d0af3d8 100644 --- a/packages/nextjs/src/server/vercelQueuesMonitoring.ts +++ b/packages/nextjs/src/server/vercelQueuesMonitoring.ts @@ -1,3 +1,4 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; import { getIsolationScope, spanToJSON } from '@sentry/core'; @@ -76,7 +77,7 @@ export function maybeEnrichQueueProducerSpan(span: Span): void { const spanData = spanToJSON(span).data; // http.client spans have url.full attribute - const urlFull = spanData?.['url.full'] as string | undefined; + const urlFull = spanData?.[URL_FULL] as string | undefined; if (!urlFull) { return; } diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 5d96e69fb39b..27e5c8e9ccca 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,12 +1,6 @@ import type { RequestOptions } from 'node:http'; import type { HttpClientRequest, HttpIncomingMessage, HttpServerResponse, Span } from '@sentry/core'; -import { - defineIntegration, - hasSpansEnabled, - SEMANTIC_ATTRIBUTE_URL_FULL, - stripDataUrlContent, - getRequestUrlFromClientRequest, -} from '@sentry/core'; +import { defineIntegration, hasSpansEnabled, stripDataUrlContent, getRequestUrlFromClientRequest } from '@sentry/core'; import type { NodeClient, SentryHttpInstrumentationOptions, @@ -14,6 +8,7 @@ import type { HttpServerSpansIntegrationOptions, } from '@sentry/node-core'; import { httpServerIntegration, httpServerSpansIntegration, instrumentHttpOutgoingRequests } from '@sentry/node-core'; +import { URL_FULL } from '@sentry/conventions/attributes'; const INTEGRATION_NAME = 'Http' as const; @@ -210,7 +205,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => // TODO(v11): Update these to the Sentry semantic attributes. // https://getsentry.github.io/sentry-conventions/attributes/ span.setAttribute('http.url', sanitizedUrl); - span.setAttribute(SEMANTIC_ATTRIBUTE_URL_FULL, sanitizedUrl); + span.setAttribute(URL_FULL, sanitizedUrl); span.updateName(`${request.method || 'GET'} ${sanitizedUrl}`); } options.instrumentation?.requestHook?.(span, request); diff --git a/packages/react-router/src/server/createServerInstrumentation.ts b/packages/react-router/src/server/createServerInstrumentation.ts index a4c90ee40f44..982345db691d 100644 --- a/packages/react-router/src/server/createServerInstrumentation.ts +++ b/packages/react-router/src/server/createServerInstrumentation.ts @@ -1,5 +1,5 @@ import { context, createContextKey } from '@opentelemetry/api'; -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; +import { HTTP_REQUEST_METHOD, HTTP_ROUTE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { debug, flushIfServerless, @@ -88,9 +88,9 @@ export function createSentryServerInstrumentation( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - 'http.request.method': info.request.method, - 'url.path': pathname, - 'url.full': info.request.url, + [HTTP_REQUEST_METHOD]: info.request.method, + [URL_PATH]: pathname, + [URL_FULL]: info.request.url, }, }, async span => {