From 209a3f96ee22f17b5b29791143543a5b3ccd8750 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 24 Jul 2026 13:27:48 +0200 Subject: [PATCH 1/2] WIP name inferral more fixes moar fixes fixxx --- .../suites/orchestrion-hapi/test.ts | 2 +- .../suites/orchestrion-mongo/test.ts | 3 +- .../suites/orchestrion-tedious/test.ts | 2 +- .../tests/tracing.dynamic.test.ts | 7 +- .../tests/server-transactions.test.ts | 2 +- .../tests/server-transactions.test.ts | 2 +- .../tests/server-transactions.test.ts | 2 +- .../tests/server-transactions.test.ts | 2 +- .../nextjs-16/tests/middleware.test.ts | 59 +++--- .../nitro-3/tests/isolation.test.ts | 1 + .../nuxt-3-min/tests/tracing.test.ts | 3 +- .../server-timing-trace-propagation.test.ts | 2 +- .../express/tracing/updateName/scenario.mjs | 8 - .../suites/express/tracing/updateName/test.ts | 21 +-- .../startSpan/basic-usage-streamed/test.ts | 3 - .../scenario-invalid-root-span.mjs | 22 --- .../scenario-multiple-operations-many.mjs | 33 ++-- .../scenario-multiple-operations.mjs | 35 ++-- .../scenario-mutation.mjs | 39 ++-- .../scenario-no-operation-name.mjs | 29 ++- .../scenario-query.mjs | 29 ++- .../useOperationNameForRootSpan/test.ts | 40 +---- .../http-otel-double-instrumentation/test.ts | 2 +- .../lru-memoizer/scenario-parallel.mjs | 4 +- .../suites/tracing/lru-memoizer/scenario.mjs | 2 +- .../suites/tracing/lru-memoizer/test.ts | 2 +- .../suites/tracing/mysql/test.ts | 4 - .../suites/tracing/postgres-streamed/test.ts | 4 - packages/astro/package.json | 3 +- packages/astro/src/server/middleware.ts | 13 +- .../integrations/http/server-subscription.ts | 3 - .../http/server-subscription.test.ts | 1 - .../nextjs/src/common/utils/tracingUtils.ts | 7 +- .../nextjs/src/server/handleOnSpanStart.ts | 36 ++-- .../src/runtime/hooks/captureErrorHook.ts | 22 +-- .../src/runtime/hooks/captureTracingEvents.ts | 37 ++-- .../http/httpServerSpansIntegration.ts | 15 +- packages/node/src/integrations/http/index.ts | 13 +- .../node-fetch/undici-instrumentation.ts | 16 +- .../src/integrations/tracing/redis/cache.ts | 2 + .../utils/setHttpServerSpanRouteAttribute.ts | 23 --- .../hooks/updateRouteBeforeResponse.ts | 10 +- .../plugins/route-detector-legacy.server.ts | 20 +-- .../runtime/plugins/route-detector.server.ts | 20 +-- .../hooks/updateRouteBeforeResponse.test.ts | 54 +++--- .../opentelemetry/src/applyOtelSpanData.ts | 37 +--- .../src/utils/backfillStreamedSpanData.ts | 11 +- ...enhanceDscWithOpenTelemetryRootSpanName.ts | 7 +- .../src/utils/parseSpanDescription.ts | 135 ++------------ .../test/integration/scope.test.ts | 3 +- .../test/integration/transactions.test.ts | 3 +- packages/opentelemetry/test/trace.test.ts | 12 +- .../opentelemetry/test/tracerProvider.test.ts | 32 +--- .../test/utils/parseSpanDescription.test.ts | 170 ++---------------- .../react-router/src/client/hydratedRouter.ts | 5 +- packages/react-router/src/client/utils.ts | 6 +- .../createClientInstrumentation.test.ts | 3 + .../test/client/hydratedRouter.test.ts | 34 +--- .../react-router/test/client/utils.test.ts | 2 + packages/react/src/reactrouter.tsx | 5 +- packages/remix/src/server/instrumentServer.ts | 3 +- .../server/integrations/tracing-channel.ts | 24 ++- .../test/server/tracing-channel-test-utils.ts | 3 +- .../remix/test/server/tracing-channel.test.ts | 7 +- packages/server-utils/src/graphql/utils.ts | 5 +- packages/server-utils/src/index.ts | 1 + .../integrations/tracing-channel/amqplib.ts | 2 + .../express/instrumentation.ts | 23 +-- .../fastify/instrumentation.ts | 18 +- .../tracing-channel/firebase/functions.ts | 6 +- .../tracing-channel/hapi-utils.ts | 37 +--- .../src/integrations/tracing-channel/knex.ts | 5 +- .../src/integrations/tracing-channel/koa.ts | 26 +-- .../src/integrations/tracing-channel/redis.ts | 5 +- .../integrations/tracing-channel/tedious.ts | 2 +- .../server-utils/src/mongodb/mongodb-span.ts | 40 +++-- .../server-utils/src/prisma/tracing-helper.ts | 7 +- .../utils/setHttpServerSpanRouteAttribute.ts | 37 ++++ .../tracing-channel/hapi-utils.test.ts | 4 +- packages/solidstart/package.json | 4 +- .../server/withServerActionInstrumentation.ts | 8 +- .../withServerActionInstrumentation.test.ts | 14 +- 82 files changed, 449 insertions(+), 956 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-invalid-root-span.mjs delete mode 100644 packages/node/src/utils/setHttpServerSpanRouteAttribute.ts create mode 100644 packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts index db1044fc9a49..db66b1431090 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts @@ -44,7 +44,7 @@ Deno.test('hapi instrumentation: orchestrion:@hapi/hapi:route channel wraps the const hapiSpan = parent.spans?.find(s => s.op === 'router.hapi'); assertExists(hapiSpan, `expected a router.hapi span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(hapiSpan!.description, 'route - /hello'); + assertEquals(hapiSpan!.description, 'GET /hello'); assertEquals(hapiSpan!.data?.['hapi.type'], 'router'); assertEquals(hapiSpan!.data?.['http.route'], '/hello'); assertEquals(hapiSpan!.data?.['sentry.origin'], 'auto.http.orchestrion.hapi'); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts index 69fd2ffdc7a1..0c5aab6820d0 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts @@ -54,8 +54,9 @@ Deno.test('mongodb instrumentation: orchestrion:mongodb:command channel produces const mongoSpan = parent.spans?.find(s => s.op === 'db'); assertExists(mongoSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(mongoSpan!.description, 'mongodb.find'); + assertEquals(mongoSpan!.description, '{"find":"?","filter":{"name":"?"}}'); assertEquals(mongoSpan!.data?.['db.system'], 'mongodb'); + assertEquals(mongoSpan!.data?.['db.statement'], '{"find":"?","filter":{"name":"?"}}'); assertEquals(mongoSpan!.data?.['db.name'], 'mydb'); assertEquals(mongoSpan!.data?.['db.mongodb.collection'], 'users'); assertEquals(mongoSpan!.data?.['db.operation'], 'find'); diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts index d413c077d5d6..8bc91a7802cf 100644 --- a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts +++ b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts @@ -53,7 +53,7 @@ Deno.test('tedious instrumentation: orchestrion:tedious:execSql channel produces const tediousSpan = parent.spans?.find(s => s.op === 'db'); assertExists(tediousSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`); - assertEquals(tediousSpan!.description, 'execSql mydb'); + assertEquals(tediousSpan!.description, 'SELECT 1'); assertEquals(tediousSpan!.data?.['db.system'], 'mssql'); assertEquals(tediousSpan!.data?.['db.name'], 'mydb'); assertEquals(tediousSpan!.data?.['db.user'], 'sa'); 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 e94dcc2957d8..15c456ed42dd 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 @@ -212,7 +212,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { // Server page request transaction - parametrized transaction name with actual URL in data expect(serverPageRequestTxn).toMatchObject({ - transaction: 'GET /user-page/myUsername123', + transaction: 'GET /user-page/[userId]', transaction_info: { source: 'route' }, contexts: { trace: { @@ -241,14 +241,13 @@ test.describe('nested SSR routes (client, server, server request)', () => { data: { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.fetch', - 'url.full': expect.stringContaining('/api/user/myUsername123.json'), 'url.full': 'http://localhost:3030/api/user/myUsername123.json', }, }); // Server HTTP request transaction expect(serverHTTPServerRequestTxn).toMatchObject({ - transaction: 'GET /api/user/myUsername123.json', + transaction: 'GET /api/user/[userId].json', transaction_info: { source: 'route' }, contexts: { trace: { @@ -300,7 +299,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { }); expect(serverPageRequestTxn).toMatchObject({ - transaction: 'GET /catchAll/hell0/whatever-do', + transaction: 'GET /catchAll/[...path]', transaction_info: { source: 'route' }, contexts: { trace: { diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts index 4213aae3e3de..7edb459f380d 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/tests/server-transactions.test.ts @@ -47,7 +47,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; - expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/'); + expect(httpServerTransaction.transaction).toBe('GET /'); expect(pageloadTransaction.transaction).toBe('/'); expect(httpServerTraceId).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts index 7a6e202de6bb..0b25be4abb34 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/server-transactions.test.ts @@ -271,7 +271,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; - expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/'); + expect(httpServerTransaction.transaction).toBe('GET /'); expect(httpServerTransaction.contexts?.trace?.data?.['http.route']).toBeUndefined(); expect(pageloadTransaction.transaction).toBe('/'); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts index e624c578ce19..6f6581d6c05e 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/tests/server-transactions.test.ts @@ -46,7 +46,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; - expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/'); + expect(httpServerTransaction.transaction).toBe('GET /'); expect(pageloadTransaction.transaction).toBe('routes/_index'); expect(httpServerTraceId).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts index 75d8fa0d2b9e..0aff4ad4c4f2 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/tests/server-transactions.test.ts @@ -46,7 +46,7 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; const pageLoadParentSpanId = pageloadTransaction.contexts?.trace?.parent_span_id; - expect(httpServerTransaction.transaction).toBe('GET http://localhost:3030/'); + expect(httpServerTransaction.transaction).toBe('GET /'); expect(pageloadTransaction.transaction).toBe('/'); expect(httpServerTraceId).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts index c0bc1512565b..5711288d1422 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts @@ -115,37 +115,34 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru if (hasHttpClientSpan) { // Check if fetch is traced as a child span of the middleware transaction - expect(middlewareTransaction.spans).toEqual( - expect.arrayContaining([ - { - data: { - 'http.request.method': 'GET', - 'http.request.method_original': 'GET', - 'http.response.status_code': 200, - 'network.peer.address': '::1', - 'network.peer.port': 3030, - 'sentry.kind': 'client', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.otel.node_fetch', - 'server.address': 'localhost', - 'server.port': 3030, - 'url.full': 'http://localhost:3030/', - 'url.path': '/', - 'url.scheme': 'http', - 'user_agent.original': 'node', - }, - description: 'GET http://localhost:3030/', - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - status: 'ok', - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ]), - ); + expect(middlewareTransaction.spans).toContainEqual({ + data: { + 'http.request.method': 'GET', + 'http.request.method_original': 'GET', + 'http.response.status_code': 200, + 'network.peer.address': '::1', + 'network.peer.port': 3030, + 'sentry.kind': 'client', + 'sentry.op': 'http.client', + 'sentry.source': 'url', + 'sentry.origin': 'auto.http.otel.node_fetch', + 'server.address': 'localhost', + 'server.port': 3030, + 'url.full': 'http://localhost:3030/', + 'url.path': '/', + 'url.scheme': 'http', + 'user_agent.original': 'node', + }, + description: 'GET http://localhost:3030/', + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); } else { // Alternatively, fetch is traced as a separate transaction, similar to Dev builds const fetchTransaction = await fetchTransactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts index 7234fa0948ca..ce5159889f78 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts @@ -3,6 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Isolation scope prevents tag leaking between requests', async ({ request }) => { const transactionEventPromise = waitForTransaction('nitro-3', event => { + console.log('event', event.transaction, event.contexts?.trace); return event?.transaction === 'GET /api/test-isolation/:id'; }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts index 869aa9670ed5..afbe237c330e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts @@ -23,7 +23,8 @@ test.describe('distributed tracing', () => { const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content'); // Parametrization does not work in Nuxt 3.7 yet (only in newer versions) - expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param' + // Without route attribute, the transaction name is not set on the baggage + expect(baggageMetaTagContent).not.toContain('sentry-transaction='); expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`); expect(baggageMetaTagContent).toContain('sentry-sampled=true'); expect(baggageMetaTagContent).toContain('sentry-sample_rate=1'); diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts index 38b53d2bcc90..c379aeffa559 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/tests/server-timing-trace-propagation.test.ts @@ -39,7 +39,7 @@ test('propagates trace context from server-timing header to client pageload', as expect(pageloadTransaction).toBeDefined(); expect(pageloadTransaction.transaction).toBe('/'); - expect(httpServerTransaction.transaction).toMatch(/^GET http:\/\/localhost:\d+\/$/); + expect(httpServerTransaction.transaction).toBe('GET /'); expect(pageloadTransaction.contexts?.trace?.trace_id).toEqual(headerTraceId); expect(pageloadTransaction.contexts?.trace?.parent_span_id).toEqual(headerSpanId); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs b/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs index 7dd863a8abfd..75afbdbf42fe 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs @@ -18,14 +18,6 @@ app.get('/test/:id/span-updateName', (_req, res) => { res.send({ response: 'response 1' }); }); -app.get('/test/:id/span-updateName-source', (_req, res) => { - const span = Sentry.getActiveSpan(); - const rootSpan = Sentry.getRootSpan(span); - rootSpan.updateName('new-name'); - rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); - res.send({ response: 'response 2' }); -}); - app.get('/test/:id/updateSpanName', (_req, res) => { const span = Sentry.getActiveSpan(); const rootSpan = Sentry.getRootSpan(span); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts index a143b939a575..5585d8d45bec 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts @@ -12,24 +12,7 @@ describe('express tracing - updateName', () => { // This test documents the unfortunate behaviour of using `span.updateName` on the server-side. // For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation // calls `span.updateName` and overwrites whatever the name was set to before (by us or by users). - test("calling just `span.updateName` doesn't update the final name in express (missing source)", async () => { - const runner = createRunner() - .expect({ - transaction: { - transaction: 'GET /test/:id/span-updateName', - transaction_info: { - source: 'route', - }, - }, - }) - .start(); - runner.makeRequest('get', '/test/123/span-updateName'); - await runner.completed(); - }); - - // Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal. - // Therefore, only the source is updated but the name is still overwritten by Otel. - test('calling `span.updateName` and setting attribute source updates the final name in express', async () => { + test('calling just `span.updateName` updates the final name in express', async () => { const runner = createRunner() .expect({ transaction: { @@ -40,7 +23,7 @@ describe('express tracing - updateName', () => { }, }) .start(); - runner.makeRequest('get', '/test/123/span-updateName-source'); + runner.makeRequest('get', '/test/123/span-updateName'); await runner.completed(); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 83d8ec942c08..a3168e455c57 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -69,7 +69,6 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' }, }, name: 'test-child-span', is_segment: false, @@ -93,7 +92,6 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' }, }, links: [ { @@ -130,7 +128,6 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' }, }, name: 'test-manual-span', is_segment: false, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-invalid-root-span.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-invalid-root-span.mjs deleted file mode 100644 index 08402aadda43..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-invalid-root-span.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sentry from '@sentry/node'; - -const tracer = Sentry.getClient().tracer; - -async function run() { - const { createApolloServer } = await import('../../apollo-server.mjs'); - const server = createApolloServer(); - - await tracer.startActiveSpan('test span name', async span => { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: 'query GetHello {hello}', - }); - - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations-many.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations-many.mjs index f69099bd19b1..8d62e2144b59 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations-many.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations-many.mjs @@ -1,31 +1,22 @@ import * as Sentry from '@sentry/node'; -const tracer = Sentry.getClient().tracer; - async function run() { const { createApolloServer } = await import('../../apollo-server.mjs'); const server = createApolloServer(); - await tracer.startActiveSpan( - 'test span name', - { - kind: 1, - attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' }, - }, - async span => { - for (let i = 1; i < 10; i++) { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: `query GetHello${i} {hello}`, - }); - } + await Sentry.startSpan({ name: 'test span name' }, async span => { + for (let i = 1; i < 10; i++) { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: `query GetHello${i} {hello}`, + }); + } - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }, - ); + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations.mjs index 99b4cce96828..a19dd9086c38 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations.mjs @@ -1,33 +1,24 @@ import * as Sentry from '@sentry/node'; -const tracer = Sentry.getClient().tracer; - async function run() { const { createApolloServer } = await import('../../apollo-server.mjs'); const server = createApolloServer(); - await tracer.startActiveSpan( - 'test span name', - { - kind: 1, - attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' }, - }, - async span => { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: 'query GetWorld {world}', - }); + await Sentry.startSpan({ name: 'test span name' }, async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: 'query GetWorld {world}', + }); - await server.executeOperation({ - query: 'query GetHello {hello}', - }); + await server.executeOperation({ + query: 'query GetHello {hello}', + }); - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }, - ); + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-mutation.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-mutation.mjs index 3779e8e73094..19fc16d3b1a7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-mutation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-mutation.mjs @@ -1,35 +1,26 @@ import * as Sentry from '@sentry/node'; import gql from 'graphql-tag'; -const tracer = Sentry.getClient().tracer; - async function run() { const { createApolloServer } = await import('../../apollo-server.mjs'); const server = createApolloServer(); - await tracer.startActiveSpan( - 'test span name', - { - kind: 1, - attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' }, - }, - async span => { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: gql` - mutation TestMutation($email: String) { - login(email: $email) - } - `, - variables: { email: 'test@email.com' }, - }); + await Sentry.startSpan({ name: 'test span name' }, async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: gql` + mutation TestMutation($email: String) { + login(email: $email) + } + `, + variables: { email: 'test@email.com' }, + }); - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }, - ); + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-no-operation-name.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-no-operation-name.mjs index 02afc51d5034..53c4e8341c50 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-no-operation-name.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-no-operation-name.mjs @@ -1,29 +1,20 @@ import * as Sentry from '@sentry/node'; -const tracer = Sentry.getClient().tracer; - async function run() { const { createApolloServer } = await import('../../apollo-server.mjs'); const server = createApolloServer(); - await tracer.startActiveSpan( - 'test span name', - { - kind: 1, - attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' }, - }, - async span => { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: 'query {hello}', - }); + await Sentry.startSpan({ name: 'test span name' }, async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: 'query {hello}', + }); - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }, - ); + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-query.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-query.mjs index ee3f579ec26e..aef3d52698ef 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-query.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-query.mjs @@ -1,29 +1,20 @@ import * as Sentry from '@sentry/node'; -const tracer = Sentry.getClient().tracer; - async function run() { const { createApolloServer } = await import('../../apollo-server.mjs'); const server = createApolloServer(); - await tracer.startActiveSpan( - 'test span name', - { - kind: 1, - attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' }, - }, - async span => { - // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation - await server.executeOperation({ - query: 'query GetHello {hello}', - }); + await Sentry.startSpan({ name: 'test span name' }, async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: 'query GetHello {hello}', + }); - setTimeout(() => { - span.end(); - server.stop(); - }, 500); - }, - ); + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts index 9daa789287a0..e7fc59b8b639 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts @@ -13,7 +13,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { describe('single query operation', () => { const EXPECTED_TRANSACTION = { - transaction: 'GET /test-graphql (query GetHello)', + transaction: 'test span name (query GetHello)', spans: expect.arrayContaining([ expect.objectContaining({ data: { @@ -43,7 +43,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { describe('single mutation operation', () => { const EXPECTED_TRANSACTION = { - transaction: 'GET /test-graphql (mutation TestMutation)', + transaction: 'test span name (mutation TestMutation)', spans: expect.arrayContaining([ expect.objectContaining({ data: { @@ -73,39 +73,9 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); }); - describe('invalid root span', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query GetHello)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetHello', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetHello {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - }, - description: 'query GetHello', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - - createEsmAndCjsTests(__dirname, 'scenario-invalid-root-span.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('useOperationNameForRootSpan ignores an invalid root span', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - }); - }); - describe('query without name', () => { const EXPECTED_TRANSACTION = { - transaction: 'GET /test-graphql (query)', + transaction: 'test span name (query)', spans: expect.arrayContaining([ expect.objectContaining({ data: { @@ -134,7 +104,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { describe('multiple operations', () => { const EXPECTED_TRANSACTION = { - transaction: 'GET /test-graphql (query GetHello, query GetWorld)', + transaction: 'test span name (query GetHello, query GetWorld)', spans: expect.arrayContaining([ expect.objectContaining({ data: { @@ -177,7 +147,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { describe('many operations', () => { const EXPECTED_TRANSACTION = { transaction: - 'GET /test-graphql (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', + 'test span name (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', }; createEsmAndCjsTests( diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-otel-double-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-otel-double-instrumentation/test.ts index b54e31a3ebbc..09cf098ccb91 100644 --- a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-otel-double-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-otel-double-instrumentation/test.ts @@ -96,7 +96,7 @@ describe('http OTel double instrumentation', () => { // http.client span. expect(httpClientSpans).toHaveLength(1); expect(httpClientSpans[0]).toMatchObject({ - description: expect.stringMatching(/GET .*\/api\/v0/), + description: 'GET', status: 'ok', }); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario-parallel.mjs b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario-parallel.mjs index 985ef267edc8..86107c1fe6f8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario-parallel.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario-parallel.mjs @@ -18,7 +18,7 @@ const run = async () => { // Concurrent calls with the same key share one in-flight load, so each span's callback is queued // against it. We record on each transaction whether its callback kept its own span's context. const first = Sentry.startSpan( - { op: 'first' }, + { name: 'test-name', op: 'first' }, firstSpan => new Promise(resolve => { memoizedFoo({ foo: 'bar' }, () => { @@ -32,7 +32,7 @@ const run = async () => { ); const second = Sentry.startSpan( - { op: 'second' }, + { name: 'test-name', op: 'second' }, secondSpan => new Promise(resolve => { memoizedFoo({ foo: 'bar' }, () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.mjs index 9415f8eb2484..eca8890bea51 100644 --- a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/scenario.mjs @@ -29,7 +29,7 @@ const run = async () => { // Keep the span open until the memoized callback fires, recording on the transaction whether the // callback ran in the originating span's context. const spanFinished = Sentry.startSpan( - { op: 'run' }, + { name: 'test-name', op: 'run' }, span => new Promise(resolve => { memoizedFoo({ foo: 'bar' }, () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts index 56f8961ea9aa..6df385b71256 100644 --- a/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts @@ -17,7 +17,7 @@ describe('lru-memoizer', () => { await createTestRunner() .expect({ transaction: { - transaction: '', + transaction: 'test-name', contexts: { trace: expect.objectContaining({ op: 'run', diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts index 0086574eea45..5819849340dc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -240,10 +240,6 @@ describe('mysql auto instrumentation', () => { type: 'string', value: 'Test Transaction', }, - 'sentry.source': { - type: 'string', - value: 'task', - }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts index 483c5f1e45a5..5878bc038059 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts @@ -68,10 +68,6 @@ const COMMON_DB_ATTRIBUTES = { type: 'string', value: 'Test Span', }, - 'sentry.source': { - type: 'string', - value: 'task', - }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', diff --git a/packages/astro/package.json b/packages/astro/package.json index 621ae4b6a381..b708ed3f06b4 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -60,7 +60,8 @@ "@sentry/core": "10.67.0", "@sentry/conventions": "^0.16.0", "@sentry/node": "10.67.0", - "@sentry/bundler-plugins": "10.67.0" + "@sentry/bundler-plugins": "10.67.0", + "@sentry/server-utils": "10.67.0" }, "devDependencies": { "astro": "^4.16.19", diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 3a2cbf05dcd3..2b846fe68788 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -28,6 +28,7 @@ import { winterCGHeadersToDict, withIsolationScope, } from '@sentry/node'; +import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; import type { APIContext, MiddlewareHandler, MiddlewareNext, RoutePart } from 'astro'; type MiddlewareOptions = { @@ -142,11 +143,7 @@ async function enhanceHttpServerSpan(ctx: APIContext, next: MiddlewareNext, root }); if (parametrizedRoute) { - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': parametrizedRoute, - }); - + setHttpServerSpanRouteAttribute(parametrizedRoute); isolationScope.setTransactionName(`${method} ${parametrizedRoute}`); } @@ -228,12 +225,14 @@ async function instrumentRequestStartHttpServerSpan( attributes[URL_QUERY] = getUrlQuery(ctx.url.search); attributes[URL_FRAGMENT] = getUrlFragment(ctx.url.hash); - isolationScope.setTransactionName(`${method} ${parametrizedRoute || ctx.url.pathname}`); + const name = `${method} ${parametrizedRoute || ctx.url.pathname}`; + + isolationScope.setTransactionName(name); const res = await startSpan( { attributes, - name: `${method} ${parametrizedRoute || ctx.url.pathname}`, + name, op: 'http.server', }, async span => { diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index e763ee591145..7b3c26b19f4f 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -285,9 +285,6 @@ function buildServerSpanWrap( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.server', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [SENTRY_KIND]: 'server', - // Set http.route to the URL path as a best-effort route name. - // Framework integrations (Express, etc.) update this via onSpanEnd. - 'http.route': httpTargetWithoutQueryFragment, // Network attributes 'net.host.ip': localAddress, 'net.host.port': localPort, diff --git a/packages/core/test/lib/integrations/http/server-subscription.test.ts b/packages/core/test/lib/integrations/http/server-subscription.test.ts index 7d019955e38c..eacefaa8df20 100644 --- a/packages/core/test/lib/integrations/http/server-subscription.test.ts +++ b/packages/core/test/lib/integrations/http/server-subscription.test.ts @@ -100,7 +100,6 @@ describe('getHttpServerSubscriptions', () => { origin: 'auto.http.server', data: expect.objectContaining({ 'http.method': 'GET', - 'http.route': '/users/42', 'http.response.status_code': 200, 'http.status_code': 200, 'http.target': '/users/42?foo=bar', diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts index 3c559a481086..916bea190470 100644 --- a/packages/nextjs/src/common/utils/tracingUtils.ts +++ b/packages/nextjs/src/common/utils/tracingUtils.ts @@ -1,4 +1,4 @@ -import { HTTP_ROUTE } from '@sentry/conventions/attributes'; +import { HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; import type { PropagationContext, Span, SpanAttributes } from '@sentry/core'; import { isObjectLike, @@ -8,9 +8,9 @@ import { getRootSpan, GLOBAL_OBJ, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, startNewTrace, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { ATTR_NEXT_SEGMENT, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; @@ -184,6 +184,7 @@ export function maybeEnhanceServerComponentSpanName( activeSpan.setAttributes({ 'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout', 'sentry.nextjs.ssr.function.route': route, + [SENTRY_OP]: 'function.nextjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }); - activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.nextjs'); } diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts index e5d056bb5f98..d4889cb90603 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -1,6 +1,12 @@ import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; -import { getIsolationScope, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; +import { + getIsolationScope, + getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, +} from '@sentry/core'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; @@ -33,18 +39,21 @@ export function handleOnSpanStart(span: Span): void { !rootSpanAttributes?.[HTTP_ROUTE] ) { const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, ''); - rootSpan.updateName(route); - rootSpan.setAttribute(HTTP_ROUTE, route); - // Preserving the original attribute despite internally not depending on it - rootSpan.setAttribute(ATTR_NEXT_ROUTE, route); + // eslint-disable-next-line typescript/no-deprecated + const method = rootSpanAttributes?.[HTTP_REQUEST_METHOD] || rootSpanAttributes?.[HTTP_METHOD]; + + const name = typeof method === 'string' ? `${method} ${route}` : route; + rootSpan.updateName(name); + rootSpan.setAttributes({ + [HTTP_ROUTE]: route, + // Preserving the original attribute despite internally not depending on it + [ATTR_NEXT_ROUTE]: route, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }); // Update the isolation scope's transaction name so that non-transaction events // (e.g. captureMessage, captureException) also get the parameterized route. - // eslint-disable-next-line typescript/no-deprecated - const method = rootSpanAttributes?.[HTTP_REQUEST_METHOD] || rootSpanAttributes?.[HTTP_METHOD]; - if (typeof method === 'string') { - getIsolationScope().setTransactionName(`${method} ${route}`); - } + getIsolationScope().setTransactionName(name); // Check if this is a Vercel cron request and start a check-in maybeStartCronCheckIn(rootSpan, route); @@ -58,8 +67,11 @@ export function handleOnSpanStart(span: Span): void { const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME]; if (typeof middlewareName === 'string') { rootSpan.updateName(middlewareName); - rootSpan.setAttribute(HTTP_ROUTE, middlewareName); - rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName); + rootSpan.setAttributes({ + [HTTP_ROUTE]: middlewareName, + [ATTR_NEXT_SPAN_NAME]: middlewareName, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }); } span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } diff --git a/packages/nitro/src/runtime/hooks/captureErrorHook.ts b/packages/nitro/src/runtime/hooks/captureErrorHook.ts index fab0c5eff05a..1e0918a4c4e8 100644 --- a/packages/nitro/src/runtime/hooks/captureErrorHook.ts +++ b/packages/nitro/src/runtime/hooks/captureErrorHook.ts @@ -1,13 +1,4 @@ -import { - captureException, - flushIfServerless, - getActiveSpan, - getClient, - getCurrentScope, - getRootSpan, - parseUrl, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, -} from '@sentry/core'; +import { captureException, flushIfServerless, getClient, parseUrl } from '@sentry/core'; import { HTTPError } from 'h3'; import type { CapturedErrorContext } from 'nitro/types'; @@ -54,17 +45,6 @@ export async function captureErrorHook(error: Error, errorContext: CapturedError return; } - const method = errorContext.event?.req.method ?? ''; - const path = errorContext.event?.req.url ? parseUrl(errorContext.event.req.url).path : null; - - if (path) { - getCurrentScope().setTransactionName(`${method} ${path}`); - const activeSpan = getActiveSpan(); - const activeRootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - activeRootSpan?.updateName(`${method} ${path}`); - activeRootSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - } - const structuredContext = extractErrorContext(errorContext); captureException(error, { diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts index 131c9d177579..155d8f1d6553 100644 --- a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts @@ -10,13 +10,15 @@ import { parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus, type Span, startInactiveSpan, - updateSpanName, } from '@sentry/core'; -import { bindTracingChannelToSpan, type TracingChannelPayloadWithSpan } from '@sentry/server-utils'; +import { + bindTracingChannelToSpan, + setHttpServerSpanRouteAttribute, + type TracingChannelPayloadWithSpan, +} from '@sentry/server-utils'; import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing'; import type { RequestEvent as SrvxRequestEvent } from 'srvx/tracing'; import { setServerTimingHeaders } from './setServerTimingHeaders'; @@ -127,17 +129,9 @@ function setupH3TracingChannels(): void { // The srvx span is created before h3 resolves the route, so it initially has the raw URL. // Note: data.type is always 'middleware' here regardless of handler type, so we rely on // getParameterizedRoute() to filter out catch-all routes instead. - const rootSpan = getRootSpan(span); - if (rootSpan && rootSpan !== span) { - const routePattern = getParameterizedRoute(data.event); - if (routePattern) { - const method = data.event.req.method || 'GET'; - updateSpanName(rootSpan, `${method} ${routePattern}`); - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': routePattern, - }); - } + const routePattern = getParameterizedRoute(data.event); + if (routePattern) { + setHttpServerSpanRouteAttribute(routePattern); } }, }, @@ -234,20 +228,17 @@ function setupSrvxTracingChannels(): void { * Sets the parameterized route attributes on the span. */ function setParameterizedRouteAttributes(span: Span, event: H3TracingRequestEvent['event']): void { - const rootSpan = getRootSpan(span); - if (!rootSpan) { - return; - } - const matchedRoutePath = getParameterizedRoute(event); if (!matchedRoutePath) { return; } - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': matchedRoutePath, - }); + setHttpServerSpanRouteAttribute(matchedRoutePath); + + const rootSpan = getRootSpan(span); + if (!rootSpan) { + return; + } const params = event.context?.params; diff --git a/packages/node/src/integrations/http/httpServerSpansIntegration.ts b/packages/node/src/integrations/http/httpServerSpansIntegration.ts index 74dc7d57eb97..9cb1e68eca90 100644 --- a/packages/node/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node/src/integrations/http/httpServerSpansIntegration.ts @@ -47,6 +47,7 @@ import { parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, stripUrlQueryAndFragment, isTracingSuppressed, @@ -178,6 +179,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions // Sentry specific attributes [SENTRY_KIND]: 'server', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, [URL_FULL]: fullUrl, @@ -228,13 +230,17 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions const newAttributes = getIncomingRequestAttributesOnResponse(request, response, rpcMetadata); span.setAttributes(newAttributes); span.setStatus(status); - span.end(); - // Update the transaction name if the route has changed - const route = newAttributes['http.route']; + // Once the route is resolved, upgrade the name/source from the raw URL to the parameterized route. + const route = newAttributes[HTTP_ROUTE]; if (route) { - getIsolationScope().setTransactionName(`${request.method?.toUpperCase() || 'GET'} ${route}`); + const name = `${request.method?.toUpperCase() || 'GET'} ${route}`; + span.updateName(name); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + getIsolationScope().setTransactionName(name); } + + span.end(); } response.on('close', () => { @@ -440,6 +446,7 @@ function getIncomingRequestAttributesOnResponse( if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) { const routeName = rpcMetadata.route; newAttributes[HTTP_ROUTE] = routeName; + newAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; } return newAttributes; diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index ca1976d1696c..a9bb3d69eae2 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -1,7 +1,13 @@ import type { RequestOptions } from 'node:http'; import type { HttpClientRequest, HttpIncomingMessage, HttpServerResponse, Span } from '@sentry/core'; import { URL_FULL } from '@sentry/conventions/attributes'; -import { defineIntegration, getRequestUrlFromClientRequest, hasSpansEnabled, stripDataUrlContent } from '@sentry/core'; +import { + defineIntegration, + getRequestUrlFromClientRequest, + hasSpansEnabled, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + stripDataUrlContent, +} from '@sentry/core'; import type { NodeClient } from '../../sdk/client'; import type { HttpServerIntegrationOptions } from './httpServerIntegration'; import { httpServerIntegration } from './httpServerIntegration'; @@ -204,8 +210,11 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) => const url = getRequestUrlFromClientRequest(request); if (url.startsWith('data:')) { const sanitizedUrl = stripDataUrlContent(url); - span.setAttribute(URL_FULL, sanitizedUrl); span.updateName(`${request.method || 'GET'} ${sanitizedUrl}`); + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: sanitizedUrl, + }); } options.instrumentation?.requestHook?.(span, request); }, diff --git a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts index f7ddd06b0888..353ad0f11a37 100644 --- a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts +++ b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts @@ -23,12 +23,15 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { debug, getClient, + getSanitizedUrlString, getSpanStatusFromHttpCode, hasSpanStreamingEnabled, isTracingSuppressed, LRUMap, + parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, startInactiveSpan, stripDataUrlContent, @@ -255,9 +258,20 @@ function onRequestCreated(config: NodeFetchOptions, { request }: RequestMessage) // emitted as a standalone transaction. This rule also lives in `SentrySampler`, but that only runs // when an OpenTelemetry SDK tracer provider is set up, so we enforce it here too, which covers // SDKs that don't use an OpenTelemetry tracer provider at all. + const isDataUrl = url.startsWith('data:'); + if (!isDataUrl) { + attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'url'; + } + const spanName = + requestMethod === '_OTHER' + ? 'HTTP' + : isDataUrl + ? `${request.method || 'GET'} ${stripDataUrlContent(url)}` + : `${requestMethod} ${getSanitizedUrlString(parseUrl(requestUrl.toString()))}`; + const client = getClient(); const span = startInactiveSpan({ - name: requestMethod === '_OTHER' ? 'HTTP' : requestMethod, + name: spanName, attributes, onlyIfParent: !client || !hasSpanStreamingEnabled(client), }); diff --git a/packages/node/src/integrations/tracing/redis/cache.ts b/packages/node/src/integrations/tracing/redis/cache.ts index dcfae6bb5f42..826f1e4e9465 100644 --- a/packages/node/src/integrations/tracing/redis/cache.ts +++ b/packages/node/src/integrations/tracing/redis/cache.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, SEMANTIC_ATTRIBUTE_CACHE_KEY, SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, truncate, } from '@sentry/core'; @@ -102,4 +103,5 @@ export const cacheResponseHook: IORedisResponseCustomAttributeFunction = ( span.updateName( _redisOptions.maxCacheKeyLength ? truncate(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription, ); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, undefined); }; diff --git a/packages/node/src/utils/setHttpServerSpanRouteAttribute.ts b/packages/node/src/utils/setHttpServerSpanRouteAttribute.ts deleted file mode 100644 index dc9219e3aae5..000000000000 --- a/packages/node/src/utils/setHttpServerSpanRouteAttribute.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; - -/** - * Set the `http.route` attribute on the root HTTP server span for the current trace. - * - * No-op when there is no active span, no root span, or the root span is not an - * `http.server` span — so framework instrumentations can call this unconditionally - * without risking attribute pollution on non-HTTP root spans. - */ -export function setHttpServerSpanRouteAttribute(route: string): void { - const activeSpan = getActiveSpan(); - if (!activeSpan) { - return; - } - const rootSpan = getRootSpan(activeSpan); - if (!rootSpan) { - return; - } - if (spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { - return; - } - rootSpan.setAttribute('http.route', route); -} diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts index 4c62604e68df..515b5350880c 100644 --- a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts +++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts @@ -1,4 +1,5 @@ -import { isObjectLike, debug, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { isObjectLike, debug, getActiveSpan, getRootSpan } from '@sentry/core'; +import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; import type { H3Event } from 'h3'; type MatchedRoute = { path?: string; route?: string }; @@ -29,6 +30,8 @@ export function updateRouteBeforeResponse(event: EventWithMatchedRoute): void { return; // Skip if the matched route is a catch-all route (handled in `route-detector.server.ts`) } + setHttpServerSpanRouteAttribute(matchedRoutePath); + const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined if (!activeSpan) { return; @@ -39,11 +42,6 @@ export function updateRouteBeforeResponse(event: EventWithMatchedRoute): void { return; } - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': matchedRoutePath, - }); - const params = event.context?.params; if (isObjectLike(params)) { diff --git a/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts b/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts index d67102576158..679cf7a0f4b4 100644 --- a/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts +++ b/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts @@ -1,7 +1,8 @@ -import { debug, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { debug } from '@sentry/core'; import { defineNuxtPlugin } from 'nuxt/app'; import type { NuxtPageSubset } from '../utils/route-extraction'; import { extractParametrizedRouteFromContext } from '../utils/route-extraction'; +import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; export default defineNuxtPlugin(nuxtApp => { nuxtApp.hooks.hook('app:rendered', async renderContext => { @@ -29,21 +30,8 @@ export default defineNuxtPlugin(nuxtApp => { return; } - const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined - - if (activeSpan && routeInfo.parametrizedRoute) { - const rootSpan = getRootSpan(activeSpan); - - if (!rootSpan) { - return; - } - - debug.log('Matched parametrized server route:', routeInfo.parametrizedRoute); - - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': routeInfo.parametrizedRoute, - }); + if (routeInfo.parametrizedRoute) { + setHttpServerSpanRouteAttribute(routeInfo.parametrizedRoute); } }); }); diff --git a/packages/nuxt/src/runtime/plugins/route-detector.server.ts b/packages/nuxt/src/runtime/plugins/route-detector.server.ts index 1ed3a2f1d36b..49109dd1e098 100644 --- a/packages/nuxt/src/runtime/plugins/route-detector.server.ts +++ b/packages/nuxt/src/runtime/plugins/route-detector.server.ts @@ -1,7 +1,8 @@ -import { debug, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { debug } from '@sentry/core'; import { defineNuxtPlugin } from 'nuxt/app'; import type { NuxtPageSubset } from '../utils/route-extraction'; import { extractParametrizedRouteFromContext } from '../utils/route-extraction'; +import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; export default defineNuxtPlugin(nuxtApp => { nuxtApp.hooks.hook('app:rendered', async renderContext => { @@ -29,21 +30,6 @@ export default defineNuxtPlugin(nuxtApp => { return; } - const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined - - if (activeSpan && routeInfo.parametrizedRoute) { - const rootSpan = getRootSpan(activeSpan); - - if (!rootSpan) { - return; - } - - debug.log('Matched parametrized server route:', routeInfo.parametrizedRoute); - - rootSpan.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': routeInfo.parametrizedRoute, - }); - } + setHttpServerSpanRouteAttribute(routeInfo.parametrizedRoute); }); }); diff --git a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts index d311ee253121..1e77f9ecb2c0 100644 --- a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts +++ b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts @@ -1,10 +1,8 @@ import { - debug, getActiveSpan, - getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - type Span, - type SpanAttributes, + SentrySpan, } from '@sentry/core'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { updateRouteBeforeResponse } from '../../../src/runtime/hooks/updateRouteBeforeResponse'; @@ -14,27 +12,25 @@ vi.mock(import('@sentry/core'), async importOriginal => { return { ...mod, - debug: { - ...mod.debug, - log: vi.fn(), - }, getActiveSpan: vi.fn(), - getRootSpan: vi.fn(), }; }); describe('updateRouteBeforeResponse', () => { - const mockRootSpan = { - setAttributes: vi.fn(), - } as unknown as Pick; + const mockRootSpan = new SentrySpan({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + }, + }); + mockRootSpan.updateName = vi.fn(); + mockRootSpan.setAttribute = vi.fn(); afterEach(() => { vi.resetAllMocks(); }); it('updates the transaction name for Nitro v2 matched routes', () => { - (getActiveSpan as Mock).mockReturnValue({} as Span); - (getRootSpan as Mock).mockReturnValue(mockRootSpan); + (getActiveSpan as Mock).mockReturnValue(mockRootSpan); updateRouteBeforeResponse({ _path: '/users/123', @@ -48,20 +44,15 @@ describe('updateRouteBeforeResponse', () => { }, } as never); - expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': '/users/:id', - } satisfies SpanAttributes); - expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - 'params.id': '123', - 'url.path.parameter.id': '123', - } satisfies SpanAttributes); - expect(debug.log).toHaveBeenCalledWith('Updated transaction name for parametrized route: /users/:id'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('http.route', '/users/:id'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('params.id', '123'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('url.path.parameter.id', '123'); + expect(mockRootSpan.updateName).toHaveBeenCalledWith('GET /users/:id'); }); it('updates the transaction name for Nitro v3 matched routes', () => { - (getActiveSpan as Mock).mockReturnValue({} as Span); - (getRootSpan as Mock).mockReturnValue(mockRootSpan); + (getActiveSpan as Mock).mockReturnValue(mockRootSpan); updateRouteBeforeResponse({ path: '/users/123', @@ -75,13 +66,10 @@ describe('updateRouteBeforeResponse', () => { }, } as never); - expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - 'http.route': '/users/:id', - } satisfies SpanAttributes); - expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - 'params.id': '123', - 'url.path.parameter.id': '123', - } satisfies SpanAttributes); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('http.route', '/users/:id'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('params.id', '123'); + expect(mockRootSpan.setAttribute).toHaveBeenCalledWith('url.path.parameter.id', '123'); + expect(mockRootSpan.updateName).toHaveBeenCalledWith('GET /users/:id'); }); }); diff --git a/packages/opentelemetry/src/applyOtelSpanData.ts b/packages/opentelemetry/src/applyOtelSpanData.ts index 1f08b4e3280a..104a2c53a82a 100644 --- a/packages/opentelemetry/src/applyOtelSpanData.ts +++ b/packages/opentelemetry/src/applyOtelSpanData.ts @@ -1,4 +1,4 @@ -import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE, SENTRY_KIND } from '@sentry/conventions/attributes'; +import { HTTP_RESPONSE_STATUS_CODE, HTTP_STATUS_CODE } from '@sentry/conventions/attributes'; import { getClient, hasSpanStreamingEnabled, @@ -41,32 +41,12 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea ? { ...attributes, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: undefined } : attributes; - const inferred = inferSpanData(spanJSON.description || '', attributesForInference); - const kind = attributes[SENTRY_KIND]; + const inferred = inferSpanData(attributesForInference); if (inferred.op && attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === undefined) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, inferred.op); } - // Don't apply 'url' source at creation time, only at span end (finalizeStatus). - // At creation, http.route may not be set yet, so inference falls back to 'url'. - // Keeping the default 'custom' source from _startRootSpan allows - // enhanceDscWithOpenTelemetryRootSpanName to include the transaction name in - // the DSC. At span end, http.route is typically available and inference returns - // 'route' instead. If it's still 'url', it's applied then. - // We also only set `source` on segment roots (spans that become transactions): - // those with no parent, plus SERVER spans, which are the segment root even when - // continuing a distributed trace (where they carry a remote `parent_span_id`). - const shouldApplyInferredSource = - inferred.source !== undefined && - inferred.source !== 'custom' && - (options.finalizeStatus || inferred.source !== 'url') && - (spanJSON.parent_span_id === undefined || kind === 'server'); - - if (shouldApplyInferredSource && (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined || canInferSource)) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, inferred.source); - } - if (inferred.data) { Object.entries(inferred.data).forEach(([key, value]) => { if (value !== undefined && attributes[key] === undefined) { @@ -80,19 +60,6 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea const client = getClient(); applyOtelSpanStatus(span, attributes, spanJSON.status, !!client && hasSpanStreamingEnabled(client)); } - - // Only re-infer the name for spans branded for OTel source inference (those the provider created - // without an explicit Sentry source). A span created with an explicit source — e.g. a Sentry - // instrumentation calling `startSpan({ name, attributes: { 'sentry.source': 'url' } })` like the Bun - // server integration — already has a deliberate name and must be left alone. Renaming it would also - // stamp `source: 'custom'` (via `updateName`), which then leaks the URL into the DSC transaction name. - if ( - mayInferSource && - inferred.description !== spanJSON.description && - (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom' || canInferSource) - ) { - span.updateName(inferred.description); - } } function applyOtelSpanStatus( diff --git a/packages/opentelemetry/src/utils/backfillStreamedSpanData.ts b/packages/opentelemetry/src/utils/backfillStreamedSpanData.ts index a77371c00b22..ab21cb954d4f 100644 --- a/packages/opentelemetry/src/utils/backfillStreamedSpanData.ts +++ b/packages/opentelemetry/src/utils/backfillStreamedSpanData.ts @@ -1,9 +1,5 @@ import type { SpanAttributes, StreamedSpanJSON } from '@sentry/core'; -import { - safeSetSpanJSONAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, -} from '@sentry/core'; +import { safeSetSpanJSONAttributes, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { inferSpanData } from './parseSpanDescription'; import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; @@ -21,13 +17,10 @@ import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; export function backfillStreamedSpanDataFromOtel(spanJSON: StreamedSpanJSON): void { const attributes = spanJSON.attributes ?? {}; - const { op, description, source, data } = inferSpanData(spanJSON.name, attributes as unknown as SpanAttributes); - - spanJSON.name = description; + const { op, data } = inferSpanData(attributes as unknown as SpanAttributes); safeSetSpanJSONAttributes(spanJSON, { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, // If nothing in the chain previously set an origin, we now default it to 'manual' // For transactions, this is done in the SpanExporter. // TODO (v11): Remove this again once we fully moved away from OTel's TracerProvider. diff --git a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts index 14353f60d993..95b886218e6f 100644 --- a/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts +++ b/packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts @@ -1,7 +1,6 @@ import type { Client } from '@sentry/core'; import { hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import { getSampledForPropagation } from './getSamplingDecision'; -import { parseSpanDescription } from './parseSpanDescription'; /** * Setup a DSC handler on the passed client, @@ -16,6 +15,7 @@ export function enhanceDscWithOpenTelemetryRootSpanName(client: Client): void { const jsonSpan = spanToJSON(rootSpan); const attributes = jsonSpan.data; const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const description = jsonSpan.description; const sampled = getSampledForPropagation(rootSpan, client); @@ -27,9 +27,8 @@ export function enhanceDscWithOpenTelemetryRootSpanName(client: Client): void { // drop it here for native (SentryTracerProvider) spans that do have a name. if (sampled === false) { delete dsc.transaction; - } else if (jsonSpan.description) { - const { description } = parseSpanDescription(rootSpan); - if (source !== 'url' && description) { + } else if (description) { + if (source !== 'url') { dsc.transaction = description; } } diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index 3d26c1075a2b..e50364fb1f3d 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -1,6 +1,5 @@ -import type { Attributes, AttributeValue } from '@opentelemetry/api'; +import type { Attributes } from '@opentelemetry/api'; import { - DB_STATEMENT, DB_SYSTEM, DB_SYSTEM_NAME, FAAS_TRIGGER, @@ -10,13 +9,12 @@ import { HTTP_TARGET, MESSAGING_SYSTEM, RPC_SERVICE, - SENTRY_GRAPHQL_OPERATION, SENTRY_KIND, URL_FRAGMENT, URL_FULL, URL_QUERY, } from '@sentry/conventions/attributes'; -import type { Span, SpanAttributes, TransactionSource } from '@sentry/core'; +import type { Span, SpanAttributes } from '@sentry/core'; import { getSanitizedUrlString, getUrlFragment, @@ -24,7 +22,6 @@ import { parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, stripUrlQueryAndFragment, @@ -32,20 +29,18 @@ import { interface SpanDescription { op: string | undefined; - description: string; - source: TransactionSource; data?: Record; } /** * Infer the op & description for a set of name, attributes and kind of a span. */ -export function inferSpanData(spanName: string, attributes: SpanAttributes): SpanDescription { +export function inferSpanData(attributes: SpanAttributes): SpanDescription { // if http.method exists, this is an http request span // eslint-disable-next-line typescript/no-deprecated const httpMethod = attributes[HTTP_REQUEST_METHOD] || attributes[HTTP_METHOD]; if (httpMethod) { - return descriptionForHttpMethod({ attributes, name: spanName }, httpMethod); + return descriptionForHttpMethod(attributes); } // eslint-disable-next-line typescript/no-deprecated @@ -57,17 +52,14 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes): Spa // If db.type exists then this is a database call span // If the Redis DB is used as a cache, the span description should not be changed if (dbSystem && !opIsCache) { - return descriptionForDbSystem({ attributes, name: spanName }); + return descriptionForDbSystem(attributes); } - const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route'; - // If rpc.service exists then this is a rpc call span. // eslint-disable-next-line typescript/no-deprecated const rpcService = attributes[RPC_SERVICE]; if (rpcService) { return { - ...getUserUpdatedNameAndSource(spanName, attributes, 'route'), op: 'rpc', }; } @@ -77,7 +69,6 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes): Spa const messagingSystem = attributes[MESSAGING_SYSTEM]; if (messagingSystem) { return { - ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute), op: 'message', }; } @@ -87,12 +78,11 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes): Spa const faasTrigger = attributes[FAAS_TRIGGER]; if (faasTrigger) { return { - ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute), op: faasTrigger.toString(), }; } - return { op: undefined, description: spanName, source: 'custom' }; + return { op: undefined }; } /** @@ -107,41 +97,29 @@ export function inferSpanData(spanName: string, attributes: SpanAttributes): Spa export function parseSpanDescription(span: Span): SpanDescription { const json = spanToJSON(span); const attributes = json.data; - const name = json.description || ''; - return inferSpanData(name, attributes); + return inferSpanData(attributes); } -function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription { +function descriptionForDbSystem(attributes: Attributes): SpanDescription { // if we already have a custom name, we don't overwrite it but only set the op const userDefinedName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; if (typeof userDefinedName === 'string') { return { op: 'db', - description: userDefinedName, - source: (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || 'custom', }; } // if we already have the source set to custom, we don't overwrite the span description but only set the op if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') { - return { op: 'db', description: name, source: 'custom' }; + return { op: 'db' }; } - // Use DB statement (Ex "SELECT * FROM table") if possible as description. - // eslint-disable-next-line typescript/no-deprecated - const statement = attributes[DB_STATEMENT]; - - const description = statement ? statement.toString() : name; - - return { op: 'db', description, source: 'task' }; + return { op: 'db' }; } /** Only exported for tests. */ -export function descriptionForHttpMethod( - { name, attributes }: { name: string; attributes: Attributes }, - httpMethod: AttributeValue, -): SpanDescription { +export function descriptionForHttpMethod(attributes: Attributes): SpanDescription { const opParts = ['http']; const kind = attributes[SENTRY_KIND]; @@ -159,26 +137,12 @@ export function descriptionForHttpMethod( opParts.push('prefetch'); } - const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes); + const { urlPath, url, query, fragment } = getSanitizedUrl(attributes); if (!urlPath) { - return { ...getUserUpdatedNameAndSource(name, attributes), op: opParts.join('.') }; + return { op: opParts.join('.') }; } - const graphqlOperationsAttribute = attributes[SENTRY_GRAPHQL_OPERATION]; - - // Ex. GET /api/users - const baseDescription = `${httpMethod} ${urlPath}`; - - // When the http span has a graphql operation, append it to the description - // We add these in the graphqlIntegration - const inferredDescription = graphqlOperationsAttribute - ? `${baseDescription} (${getGraphqlOperationNamesFromAttribute(graphqlOperationsAttribute)})` - : baseDescription; - - // If `httpPath` is a root path, then we can categorize the transaction source as route. - const inferredSource: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url'; - const data: Record = {}; if (url) { @@ -193,52 +157,12 @@ export function descriptionForHttpMethod( data[URL_FRAGMENT] = urlFragment; } - // If the span kind is neither client nor server, we use the original name - // this infers that somebody manually started this span, in which case we don't want to overwrite the name - const isClientOrServerKind = kind === 'client' || kind === 'server'; - - // If the span is an auto-span (=it comes from one of our instrumentations), - // we always want to infer the name - // this is necessary because some of the auto-instrumentation we use uses kind=INTERNAL - const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual'; - const isManualSpan = !`${origin}`.startsWith('auto'); - - // If users (or in very rare occasions we) set the source to custom, we don't overwrite the name - const alreadyHasCustomSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom'; - const customSpanName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; - - const useInferredDescription = - !alreadyHasCustomSource && customSpanName == null && (isClientOrServerKind || !isManualSpan); - - const { description, source } = useInferredDescription - ? { description: inferredDescription, source: inferredSource } - : getUserUpdatedNameAndSource(name, attributes); - return { op: opParts.join('.'), - description, - source, data, }; } -function getGraphqlOperationNamesFromAttribute(attr: AttributeValue): string { - if (Array.isArray(attr)) { - // oxlint-disable-next-line typescript/require-array-sort-compare - const sorted = attr.slice().sort(); - - // Up to 5 items, we just add all of them - if (sorted.length <= 5) { - return sorted.join(', '); - } else { - // Else, we add the first 5 and the diff of other operations - return `${sorted.slice(0, 5).join(', ')}, +${sorted.length - 5}`; - } - } - - return `${attr}`; -} - /** Exported for tests only */ export function getSanitizedUrl(attributes: Attributes): { url: string | undefined; @@ -281,36 +205,3 @@ export function getSanitizedUrl(attributes: Attributes): { return { urlPath: undefined, url, query, fragment, hasRoute: false }; } - -/** - * Because Otel instrumentation sometimes mutates span names via `span.updateName`, the only way - * to ensure that a user-set span name is preserved is to store it as a tmp attribute on the span. - * We delete this attribute once we're done with it when preparing the event envelope. - * - * This temp attribute always takes precedence over the original name. - * - * We also need to take care of setting the correct source. Users can always update the source - * after updating the name, so we need to respect that. - * - * @internal exported only for testing - */ -export function getUserUpdatedNameAndSource( - originalName: string, - attributes: Attributes, - fallbackSource: TransactionSource = 'custom', -): { - description: string; - source: TransactionSource; -} { - const source = (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || fallbackSource; - const description = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; - - if (description && typeof description === 'string') { - return { - description, - source, - }; - } - - return { description: originalName, source }; -} diff --git a/packages/opentelemetry/test/integration/scope.test.ts b/packages/opentelemetry/test/integration/scope.test.ts index adebca6cd0fd..b79d6c37464d 100644 --- a/packages/opentelemetry/test/integration/scope.test.ts +++ b/packages/opentelemetry/test/integration/scope.test.ts @@ -107,8 +107,8 @@ describe('Integration | Scope', () => { trace: { data: { 'sentry.origin': 'manual', - 'sentry.source': 'custom', 'sentry.sample_rate': 1, + 'sentry.source': 'custom', }, span_id: spanId, status: 'ok', @@ -125,7 +125,6 @@ describe('Integration | Scope', () => { tag4: 'val4', }, timestamp: expect.any(Number), - transaction_info: { source: 'custom' }, type: 'transaction', }), { diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 90ca06232dfa..da4c67e4d5c8 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -270,9 +270,9 @@ describe('Integration | Transactions', () => { data: { 'sentry.op': 'test op b', 'sentry.origin': 'manual', - 'sentry.source': 'custom', 'test.outer': 'test value b', 'sentry.sample_rate': 1, + 'sentry.source': 'custom', }, op: 'test op b', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -288,7 +288,6 @@ describe('Integration | Transactions', () => { }, timestamp: expect.any(Number), transaction: 'test name b', - transaction_info: { source: 'custom' }, type: 'transaction', }), { diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 8372d5e5a497..7073850d7690 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -460,9 +460,9 @@ describe('trace', () => { expect(outerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.sample_rate': 1, 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -485,8 +485,8 @@ describe('trace', () => { expect(innerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -788,9 +788,9 @@ describe('trace', () => { expect(outerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.sample_rate': 1, 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -813,8 +813,8 @@ describe('trace', () => { expect(innerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -1173,9 +1173,9 @@ describe('trace', () => { expect(outerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.sample_rate': 1, 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -1198,8 +1198,8 @@ describe('trace', () => { expect(innerTransaction?.contexts?.trace).toEqual({ data: { - 'sentry.source': 'custom', 'sentry.origin': 'manual', + 'sentry.source': 'custom', }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/opentelemetry/test/tracerProvider.test.ts b/packages/opentelemetry/test/tracerProvider.test.ts index 6106aeb213f4..65f2ae1f76b6 100644 --- a/packages/opentelemetry/test/tracerProvider.test.ts +++ b/packages/opentelemetry/test/tracerProvider.test.ts @@ -16,7 +16,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { applyOtelSpanData } from '../src/applyOtelSpanData'; import { mockSdkInit } from './helpers/mockSdkInit'; import { init as initTestClient } from './helpers/TestClient'; -import { URL_FULL } from '@sentry/conventions/attributes'; describe('SentryTracerProvider', () => { beforeEach(() => { @@ -36,11 +35,11 @@ describe('SentryTracerProvider', () => { 'sentry.origin': 'manual', 'sentry.op': 'db', 'sentry.sample_rate': 1, - 'sentry.source': 'task', 'db.system.name': 'postgresql', 'db.statement': 'SELECT * FROM users', + 'sentry.source': 'custom', }, - description: 'SELECT * FROM users', + description: 'SELECT users', op: 'db', origin: 'manual', parent_span_id: undefined, @@ -198,7 +197,7 @@ describe('SentryTracerProvider', () => { expect(streamed.attributes?.['sentry.status.message']).toBe('Cannot enqueue Query after fatal error.'); }); - it('infers route source, op, and name for HTTP server spans', () => { + it('infers op for HTTP server spans', () => { const span = trace.getTracer('test').startSpan('GET', { kind: SpanKind.SERVER, attributes: { @@ -209,30 +208,5 @@ describe('SentryTracerProvider', () => { const json = spanToJSON(span as Span); expect(json.op).toBe('http.server'); - expect(json.data?.['sentry.source']).toBe('route'); - expect(json.description).toBe('GET /my-path/:id'); - }); - - it('defers url source to span end, keeping custom for the DSC at creation', () => { - const span = trace.getTracer('test').startSpan('POST', { - kind: SpanKind.SERVER, - attributes: { - 'http.method': 'POST', - [URL_FULL]: 'https://www.example.com/my-path', - 'http.target': '/my-path', - }, - }); - - // At creation op and name are inferred, but the `url` source is intentionally - // deferred so the default `custom` source survives for the DSC transaction name - // (http.route is often not available yet at this point). - const atCreation = spanToJSON(span as Span); - expect(atCreation.op).toBe('http.server'); - expect(atCreation.description).toBe('POST /my-path'); - expect(atCreation.data?.['sentry.source']).toBe('custom'); - - // At span end the inferred `url` source is applied. - applyOtelSpanData(span as Span, { finalizeStatus: true }); - expect(spanToJSON(span as Span).data?.['sentry.source']).toBe('url'); }); }); diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts index 4334c4727367..63308d2ebdfa 100644 --- a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -14,25 +14,24 @@ import { SENTRY_KIND, URL_FULL, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SentrySpan } from '@sentry/core'; import { describe, expect, it } from 'vitest'; -import { - descriptionForHttpMethod, - getSanitizedUrl, - getUserUpdatedNameAndSource, - inferSpanData, -} from '../../src/utils/parseSpanDescription'; +import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription'; describe('inferSpanData', () => { it.each([ + [ + 'works without attributes & name', + undefined, + { + op: undefined, + }, + ], [ 'works with empty attributes', {}, - 'test name', { - description: 'test name', op: undefined, - source: 'custom', }, ], [ @@ -41,11 +40,8 @@ describe('inferSpanData', () => { [HTTP_METHOD]: 'GET', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'test name', op: 'http.client', - source: 'custom', }, ], [ @@ -54,11 +50,8 @@ describe('inferSpanData', () => { 'http.request.method': 'GET', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'test name', op: 'http.client', - source: 'custom', }, ], [ @@ -68,11 +61,8 @@ describe('inferSpanData', () => { [DB_STATEMENT]: 'SELECT * from users', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'SELECT * from users', op: 'db', - source: 'task', }, ], [ @@ -83,11 +73,8 @@ describe('inferSpanData', () => { [DB_STATEMENT]: 'SELECT * from users', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'test name', op: 'db', - source: 'custom', }, ], [ @@ -99,11 +86,8 @@ describe('inferSpanData', () => { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'custom name', op: 'db', - source: 'custom', }, ], [ @@ -115,11 +99,8 @@ describe('inferSpanData', () => { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'custom name', op: 'db', - source: 'component', }, ], [ @@ -128,11 +109,8 @@ describe('inferSpanData', () => { [DB_SYSTEM]: 'mysql', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'test name', op: 'db', - source: 'task', }, ], [ @@ -142,11 +120,8 @@ describe('inferSpanData', () => { [DB_STATEMENT]: 'SELECT * from users', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'SELECT * from users', op: 'db', - source: 'task', }, ], [ @@ -155,11 +130,8 @@ describe('inferSpanData', () => { [DB_SYSTEM_NAME]: 'postgresql', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'test name', op: 'db', - source: 'task', }, ], [ @@ -170,11 +142,8 @@ describe('inferSpanData', () => { [DB_STATEMENT]: 'SELECT * from users', [SENTRY_KIND]: 'client', }, - 'test name', { - description: 'SELECT * from users', op: 'db', - source: 'task', }, ], [ @@ -182,11 +151,8 @@ describe('inferSpanData', () => { { [RPC_SERVICE]: 'rpc-test-service', }, - 'test name', { - description: 'test name', op: 'rpc', - source: 'route', }, ], [ @@ -195,11 +161,8 @@ describe('inferSpanData', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [RPC_SERVICE]: 'rpc-test-service', }, - 'test name', { - description: 'test name', op: 'rpc', - source: 'custom', }, ], [ @@ -209,11 +172,8 @@ describe('inferSpanData', () => { [RPC_SERVICE]: 'rpc-test-service', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'rpc', - source: 'custom', }, ], [ @@ -223,11 +183,8 @@ describe('inferSpanData', () => { [RPC_SERVICE]: 'rpc-test-service', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'rpc', - source: 'component', }, ], [ @@ -235,11 +192,8 @@ describe('inferSpanData', () => { { [MESSAGING_SYSTEM]: 'test-messaging-system', }, - 'test name', { - description: 'test name', op: 'message', - source: 'route', }, ], [ @@ -248,11 +202,8 @@ describe('inferSpanData', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [MESSAGING_SYSTEM]: 'test-messaging-system', }, - 'test name', { - description: 'test name', op: 'message', - source: 'custom', }, ], [ @@ -262,11 +213,8 @@ describe('inferSpanData', () => { [MESSAGING_SYSTEM]: 'test-messaging-system', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'message', - source: 'custom', }, ], [ @@ -276,11 +224,8 @@ describe('inferSpanData', () => { [MESSAGING_SYSTEM]: 'test-messaging-system', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'message', - source: 'component', }, ], [ @@ -288,11 +233,8 @@ describe('inferSpanData', () => { { [FAAS_TRIGGER]: 'test-faas-trigger', }, - 'test name', { - description: 'test name', op: 'test-faas-trigger', - source: 'route', }, ], [ @@ -301,11 +243,8 @@ describe('inferSpanData', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [FAAS_TRIGGER]: 'test-faas-trigger', }, - 'test name', { - description: 'test name', op: 'test-faas-trigger', - source: 'custom', }, ], [ @@ -315,11 +254,8 @@ describe('inferSpanData', () => { [FAAS_TRIGGER]: 'test-faas-trigger', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'test-faas-trigger', - source: 'custom', }, ], [ @@ -329,15 +265,12 @@ describe('inferSpanData', () => { [FAAS_TRIGGER]: 'test-faas-trigger', [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', }, - 'test name', { - description: 'custom name', op: 'test-faas-trigger', - source: 'component', }, ], - ])('%s', (_, attributes, name, expected) => { - const actual = inferSpanData(name, attributes); + ] as const)('%s', (_, attributes, expected) => { + const actual = parseSpanDescription(new SentrySpan({ attributes })); expect(actual).toEqual(expected); }); }); @@ -346,37 +279,28 @@ describe('descriptionForHttpMethod', () => { it.each([ [ 'works without attributes', - 'GET', {}, - 'test name', { op: 'http', - description: 'test name', - source: 'custom', }, ], [ 'works with basic client GET', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'GET https://www.example.com/my-path', data: { 'url.full': 'https://www.example.com/my-path', }, - source: 'url', }, ], [ 'works with prefetch request', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path', @@ -384,38 +308,30 @@ describe('descriptionForHttpMethod', () => { 'sentry.http.prefetch': true, [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client.prefetch', - description: 'GET https://www.example.com/my-path', data: { 'url.full': 'https://www.example.com/my-path', }, - source: 'url', }, ], [ 'works with basic server POST', - 'POST', { [HTTP_METHOD]: 'POST', [URL_FULL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', [SENTRY_KIND]: 'server' as const, }, - 'test name', { op: 'http.server', - description: 'POST /my-path', data: { 'url.full': 'https://www.example.com/my-path', }, - source: 'url', }, ], [ 'works with client GET with route', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path/123', @@ -423,37 +339,29 @@ describe('descriptionForHttpMethod', () => { [HTTP_ROUTE]: '/my-path/:id', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'GET /my-path/:id', data: { 'url.full': 'https://www.example.com/my-path/123', }, - source: 'route', }, ], [ 'works with basic client GET without span kind', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path', [HTTP_TARGET]: '/my-path', }, - 'test name', { op: 'http', - description: 'test name', data: { 'url.full': 'https://www.example.com/my-path', }, - source: 'custom', }, ], [ "doesn't overwrite span name with source custom", - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path/123', @@ -462,19 +370,15 @@ describe('descriptionForHttpMethod', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'test name', data: { 'url.full': 'https://www.example.com/my-path/123', }, - source: 'custom', }, ], [ 'takes user-passed span name (with source custom)', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path/123', @@ -484,19 +388,15 @@ describe('descriptionForHttpMethod', () => { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'custom name', data: { 'url.full': 'https://www.example.com/my-path/123', }, - source: 'custom', }, ], [ 'takes user-passed span name (with source component)', - 'GET', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path/123', @@ -506,39 +406,32 @@ describe('descriptionForHttpMethod', () => { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'custom name', data: { 'url.full': 'https://www.example.com/my-path/123', }, - source: 'component', }, ], [ - 'strips the leading `?`/`#` for url.query and url.fragment', - 'GET', + 'strips the leading `?`/`#` from http.query and http.fragment', { [HTTP_METHOD]: 'GET', [URL_FULL]: 'https://www.example.com/my-path?id=1#section', [HTTP_TARGET]: '/my-path?id=1#section', [SENTRY_KIND]: 'client' as const, }, - 'test name', { op: 'http.client', - description: 'GET https://www.example.com/my-path', data: { 'url.full': 'https://www.example.com/my-path', 'url.query': 'id=1', 'url.fragment': 'section', }, - source: 'url', }, ], - ])('%s', (_, httpMethod, attributes, name, expected) => { - const actual = descriptionForHttpMethod({ attributes, name }, httpMethod); + ])('%s', (_, attributes, expected) => { + const actual = descriptionForHttpMethod(attributes); expect(actual).toEqual(expected); }); }); @@ -689,38 +582,3 @@ describe('getSanitizedUrl', () => { expect(actual).toEqual(expected); }); }); - -describe('getUserUpdatedNameAndSource', () => { - it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => { - expect(getUserUpdatedNameAndSource('base name', {})).toEqual({ description: 'base name', source: 'custom' }); - }); - - it('returns param name with custom fallback source if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => { - expect(getUserUpdatedNameAndSource('base name', {}, 'route')).toEqual({ - description: 'base name', - source: 'route', - }); - }); - - it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not a string', () => { - expect(getUserUpdatedNameAndSource('base name', { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 123 })).toEqual({ - description: 'base name', - source: 'custom', - }); - }); - - it.each(['custom', 'task', 'url', 'route'])( - 'returns `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute if is a string and source is %s', - source => { - expect( - getUserUpdatedNameAndSource('base name', { - [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, - }), - ).toEqual({ - description: 'custom name', - source, - }); - }, - ); -}); diff --git a/packages/react-router/src/client/hydratedRouter.ts b/packages/react-router/src/client/hydratedRouter.ts index d0fbb7b04f29..bac743956db9 100644 --- a/packages/react-router/src/client/hydratedRouter.ts +++ b/packages/react-router/src/client/hydratedRouter.ts @@ -150,7 +150,10 @@ export function instrumentHydratedRouter(): void { (spanPathname && destinationPathname === normalizePathname(spanPathname))) ) { rootSpan.updateName(parameterizedRoute); - rootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_TEMPLATE]: parameterizedRoute }); + rootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [URL_TEMPLATE]: parameterizedRoute, + }); } }); return true; diff --git a/packages/react-router/src/client/utils.ts b/packages/react-router/src/client/utils.ts index 0c670f454c3d..58c367d00c4b 100644 --- a/packages/react-router/src/client/utils.ts +++ b/packages/react-router/src/client/utils.ts @@ -104,6 +104,7 @@ export function updateNavigationSpanUrlFromLocation(span: Span): void { span.updateName(pathname); span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [URL_PATH]: pathname, [URL_FULL]: destinationUrl, }); @@ -145,7 +146,10 @@ export function finalizeNavigationSpanFromRouterState(span: Span, routerState: R ) { const parameterizedRoute = getParameterizedRoute(routerState); span.updateName(parameterizedRoute); - span.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_TEMPLATE]: parameterizedRoute }); + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [URL_TEMPLATE]: parameterizedRoute, + }); } } diff --git a/packages/react-router/test/client/createClientInstrumentation.test.ts b/packages/react-router/test/client/createClientInstrumentation.test.ts index 350871dbb5e7..0b1df1769e75 100644 --- a/packages/react-router/test/client/createClientInstrumentation.test.ts +++ b/packages/react-router/test/client/createClientInstrumentation.test.ts @@ -445,6 +445,7 @@ describe('createSentryClientInstrumentation', () => { ); expect(mockNavigationSpan.updateName).toHaveBeenCalledWith(destination); expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': destination, 'url.full': `https://example.com${destination}`, }); @@ -486,6 +487,7 @@ describe('createSentryClientInstrumentation', () => { await hooks.navigate(mockCallNavigate, { currentUrl: '/current-page', to: -1 }); expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/previous-page', 'url.full': 'https://example.com/previous-page', }); @@ -751,6 +753,7 @@ describe('createSentryClientInstrumentation', () => { // Only ONE span created (not two - no duplicate from popstate) expect(browser.startBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); expect(mockNavigationSpan.setAttributes).toHaveBeenLastCalledWith({ + 'sentry.source': 'url', 'url.path': '/previous-page', 'url.full': 'https://example.com/previous-page', }); diff --git a/packages/react-router/test/client/hydratedRouter.test.ts b/packages/react-router/test/client/hydratedRouter.test.ts index f425b7e80c21..e0a9596231db 100644 --- a/packages/react-router/test/client/hydratedRouter.test.ts +++ b/packages/react-router/test/client/hydratedRouter.test.ts @@ -15,9 +15,9 @@ vi.mock('@sentry/core', async () => { debug: { warn: vi.fn(), }, - SEMANTIC_ATTRIBUTE_SENTRY_OP: 'op', - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'origin', - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'source', + SEMANTIC_ATTRIBUTE_SENTRY_OP: 'sentry.op', + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin', + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source', GLOBAL_OBJ: globalThis, }; }); @@ -139,31 +139,6 @@ describe('instrumentHydratedRouter', () => { }); }); - it('skips the subscribe heuristic when the API is active and a route hook already set source:route', () => { - // When a native route hook has parameterized the navigation root (source:route), the legacy - // heuristic must not re-rename it. - (globalThis as any).__sentryReactRouterClientInstrumentationUsed = true; - (core.spanToJSON as any).mockImplementation((span: any) => ({ - description: '/foo/bar', - op: span === mockNavigationSpan ? 'navigation' : 'pageload', - data: { source: 'route' }, - })); - - instrumentHydratedRouter(); - const callback = mockRouter.subscribe.mock.calls[0][0]; - const newState = { - location: { pathname: '/foo/bar' }, - matches: [{ route: { path: '/foo/:id' } }], - navigation: { state: 'idle' }, - }; - (core.getActiveSpan as any).mockReturnValue(mockNavigationSpan); - callback(newState); - - expect(mockNavigationSpan.updateName).not.toHaveBeenCalled(); - - delete (globalThis as any).__sentryReactRouterClientInstrumentationUsed; - }); - it('still parameterizes a navigation root via subscribe (backstop) when the API is active but the route had no hook (source:url)', () => { // Routes without a loader/action never trigger a route hook, so the navigation root is still // source:url. The heuristic must still parameterize it instead of leaving the raw URL. @@ -306,6 +281,7 @@ describe('instrumentHydratedRouter', () => { expect(mockNavigationSpan.updateName).toHaveBeenCalledWith('/foo'); expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/foo', 'url.full': 'https://example.com/foo', }); @@ -338,6 +314,7 @@ describe('instrumentHydratedRouter', () => { await navigateResult.catch(() => undefined); expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/foo', 'url.full': 'https://example.com/foo', }); @@ -356,6 +333,7 @@ describe('instrumentHydratedRouter', () => { expect(mockNavigationSpan.updateName).toHaveBeenCalledWith('/foo'); expect(mockNavigationSpan.updateName).toHaveBeenCalledTimes(1); expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/foo', 'url.full': 'https://example.com/foo', }); diff --git a/packages/react-router/test/client/utils.test.ts b/packages/react-router/test/client/utils.test.ts index 858f1a595e59..5839e934e3cd 100644 --- a/packages/react-router/test/client/utils.test.ts +++ b/packages/react-router/test/client/utils.test.ts @@ -117,6 +117,7 @@ describe('updateNavigationSpanUrlFromLocation', () => { expect(span.updateName).toHaveBeenCalledWith('/foo'); expect(span.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/foo', 'url.full': 'https://example.com/foo?bar=1#section', }); @@ -172,6 +173,7 @@ describe('finalizeNavigationSpanFromRouterState', () => { expect(span.updateName).toHaveBeenCalledWith('/performance/'); expect(span.updateName).toHaveBeenCalledTimes(1); expect(span.setAttributes).toHaveBeenCalledWith({ + 'sentry.source': 'url', 'url.path': '/performance/', 'url.full': 'https://example.com/performance/', }); diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 1ac4c7b265ee..90713d0c0bdd 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -236,7 +236,10 @@ export function withSentryRouting

, R extends React if (activeRootSpan) { activeRootSpan.updateName(route); - activeRootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [URL_TEMPLATE]: route }); + activeRootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [URL_TEMPLATE]: route, + }); } } diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index d29b27d9a476..820e524f578e 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -179,6 +179,7 @@ function updateSpanWithRoute(args: DataFunctionArgs, build: ServerBuild): void { const newSpanName = currentSpanName?.startsWith(method) ? `${method} ${transactionName}` : transactionName; rootSpan.updateName(newSpanName); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); if (source === 'route') { rootSpan.setAttribute(HTTP_ROUTE, transactionName); } @@ -377,7 +378,7 @@ function wrapRequestHandler ServerBuild | Promise const parentSpan = getActiveSpan(); const rootSpan = parentSpan && getRootSpan(parentSpan); rootSpan?.updateName(name); - + rootSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); return startSpan( { name, diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 26837beaa3ab..4b14e4b2aeca 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -7,6 +7,8 @@ import { parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -115,7 +117,10 @@ function enrichActiveSpanWithRoute(result: unknown): void { if (route?.path) { // oxlint-disable-next-line typescript/no-deprecated span.setAttribute(HTTP_ROUTE, route.path); - span.updateName(`remix.request ${route.path}`); + // oxlint-disable-next-line typescript/no-deprecated + const method = spanToJSON(span).data[HTTP_METHOD]; + span.updateName(typeof method === 'string' ? `${method} ${route.path}` : route.path); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } if (route?.id) { span.setAttribute(MATCH_ROUTE_ID, route.id); @@ -125,17 +130,24 @@ function enrichActiveSpanWithRoute(result: unknown): void { function subscribeRequestHandler(): void { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_REQUEST_HANDLER), - data => - startInactiveSpan({ - name: 'remix.request', + data => { + const requestAttributes = getRequestAttributes(data.arguments[0]); + // oxlint-disable-next-line typescript/no-deprecated + const method = requestAttributes[HTTP_METHOD]; + const path = requestAttributes[URL_PATH]; + const hasUrlName = typeof method === 'string' && typeof path === 'string'; + return startInactiveSpan({ + name: hasUrlName ? `${method} ${path}` : 'remix.request', attributes: { [SENTRY_KIND]: 'server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + ...(hasUrlName && { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }), [CODE_FUNCTION]: 'requestHandler', - ...getRequestAttributes(data.arguments[0]), + ...requestAttributes, }, - }), + }); + }, { beforeSpanEnd: (span, data) => setResponseStatus(span, data.result), }, diff --git a/packages/remix/test/server/tracing-channel-test-utils.ts b/packages/remix/test/server/tracing-channel-test-utils.ts index 37d409a25841..a2325823ab5e 100644 --- a/packages/remix/test/server/tracing-channel-test-utils.ts +++ b/packages/remix/test/server/tracing-channel-test-utils.ts @@ -65,13 +65,14 @@ export function teardownTestAsyncContextStrategy(): void { vi.restoreAllMocks(); } -export function makeSpan(): Span { +export function makeSpan(data: Record = {}): Span { return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn(), setAttribute: vi.fn(), updateName: vi.fn(), + getSpanJSON: () => ({ data }), } as unknown as Span; } diff --git a/packages/remix/test/server/tracing-channel.test.ts b/packages/remix/test/server/tracing-channel.test.ts index 2db4ac97bc39..59255cec8cc5 100644 --- a/packages/remix/test/server/tracing-channel.test.ts +++ b/packages/remix/test/server/tracing-channel.test.ts @@ -43,11 +43,12 @@ describe('remixIntegration (Orchestrion-based)', () => { expect(startInactiveSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - name: 'remix.request', + name: 'GET /users', attributes: expect.objectContaining({ 'sentry.origin': 'auto.http.orchestrion.remix', 'sentry.kind': 'server', 'sentry.op': 'http.server', + 'sentry.source': 'url', 'code.function': 'requestHandler', 'http.method': 'GET', 'url.full': 'http://localhost/users', @@ -59,6 +60,7 @@ describe('remixIntegration (Orchestrion-based)', () => { }); it('matchServerRoutes: enriches the active request span with the matched route', () => { + span = makeSpan({ 'http.method': 'GET' }); getActiveSpanSpy.mockReturnValue(span); const ctx = { arguments: [[], '/users/123'], @@ -69,7 +71,8 @@ describe('remixIntegration (Orchestrion-based)', () => { expect(span.setAttribute).toHaveBeenCalledWith('http.route', 'users/:userId'); expect(span.setAttribute).toHaveBeenCalledWith('match.route.id', 'routes/users.$userId'); - expect(span.updateName).toHaveBeenCalledWith('remix.request users/:userId'); + expect(span.updateName).toHaveBeenCalledWith('GET users/:userId'); + expect(span.setAttribute).toHaveBeenCalledWith('sentry.source', 'route'); }); it('matchServerRoutes: does nothing when there is no active span', () => { diff --git a/packages/server-utils/src/graphql/utils.ts b/packages/server-utils/src/graphql/utils.ts index 97464c4d7311..96e2cc0a7d35 100644 --- a/packages/server-utils/src/graphql/utils.ts +++ b/packages/server-utils/src/graphql/utils.ts @@ -1,6 +1,6 @@ import { SENTRY_GRAPHQL_OPERATION } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; -import { getClient, isObjectLike, getRootSpan, spanToJSON } from '@sentry/core'; +import { getClient, isObjectLike, getRootSpan, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; // Same key the OTel path uses, so renames stay consistent across both. const ORIGINAL_DESCRIPTION_ATTRIBUTE = 'original-description'; @@ -61,7 +61,10 @@ export function renameRootSpanWithOperation(span: Span, operationType: string, o rootSpan.setAttribute(ORIGINAL_DESCRIPTION_ATTRIBUTE, originalDescription); } + // `updateName` stamps `source: 'custom'`, so re-set the original source afterwards to preserve it. + const source = rootSpanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; rootSpan.updateName(`${originalDescription} (${getGraphqlOperationNamesFromAttribute(operations)})`); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } /** Format the accumulated operations for the root span name: up to 5 sorted names, then `+N`. */ diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index ec61e9492588..06352b7a0601 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -36,3 +36,4 @@ export { // oxlint-disable-next-line typescript/no-deprecated instrumentFastify, } from './integrations/tracing-channel/fastify'; +export { setHttpServerSpanRouteAttribute } from './utils/setHttpServerSpanRouteAttribute'; diff --git a/packages/server-utils/src/integrations/tracing-channel/amqplib.ts b/packages/server-utils/src/integrations/tracing-channel/amqplib.ts index 52d17d613810..ac5219d5e3de 100644 --- a/packages/server-utils/src/integrations/tracing-channel/amqplib.ts +++ b/packages/server-utils/src/integrations/tracing-channel/amqplib.ts @@ -7,6 +7,7 @@ import { defineIntegration, getTraceData, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, startInactiveSpan, timestampInSeconds, @@ -515,6 +516,7 @@ function startConsumeSpan(queue: string, msg: ConsumeMessage, channel: ChannelLi op: 'message', attributes: { [SENTRY_KIND]: 'consumer', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', ...getStoredConnectionAttributes(channel), [ATTR_MESSAGING_DESTINATION]: msg.fields?.exchange, // TODO(v11) remove this attribute [MESSAGING_DESTINATION_NAME]: msg.fields?.exchange, diff --git a/packages/server-utils/src/integrations/tracing-channel/express/instrumentation.ts b/packages/server-utils/src/integrations/tracing-channel/express/instrumentation.ts index a2bff57eb0ab..60950d23aa45 100644 --- a/packages/server-utils/src/integrations/tracing-channel/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/tracing-channel/express/instrumentation.ts @@ -6,10 +6,8 @@ import { getActiveSpan, getDefaultIsolationScope, getIsolationScope, - getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, startInactiveSpan, stringMatchesSomePattern, } from '@sentry/core'; @@ -34,6 +32,7 @@ import type { HandleChannelContext, RegistrationChannelContext, } from './types'; +import { setHttpServerSpanRouteAttribute } from '../../../utils/setHttpServerSpanRouteAttribute'; const ORIGIN = 'auto.http.express'; @@ -192,7 +191,7 @@ function getSpanForLayer(data: HandleChannelContext, options: ExpressIntegration // check, so the transaction is still named even when the layer's own span is // ignored — matches the OTel Express integration's `onRouteResolved` timing. if (matchedRoute) { - setHttpServerSpanRoute(matchedRoute); + setHttpServerSpanRouteAttribute(matchedRoute); } if (type === 'request_handler' && constructedRoute) { @@ -257,24 +256,6 @@ function getLayerType(layer: ExpressLayer): ExpressLayerType { return 'middleware'; } -/** - * Propagate the resolved route to the root `http.server` span so the - * transaction gets a parameterized `http.route`. Mirrors `@sentry/node`'s - * `setHttpServerSpanRouteAttribute`; inlined to keep this package free of - * `@sentry/node` deps. No-op unless the root span is an `http.server` span. - */ -function setHttpServerSpanRoute(route: string): void { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - if (!rootSpan) { - return; - } - if (spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { - return; - } - rootSpan.setAttribute(HTTP_ROUTE, route); -} - /** * Whether a layer should be skipped per the `ignoreLayers`/`ignoreLayersType` * options. Matches `@sentry/core`'s Express `isLayerIgnored`: `ignoreLayersType` diff --git a/packages/server-utils/src/integrations/tracing-channel/fastify/instrumentation.ts b/packages/server-utils/src/integrations/tracing-channel/fastify/instrumentation.ts index c5c43d61befb..1964307844c4 100644 --- a/packages/server-utils/src/integrations/tracing-channel/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/tracing-channel/fastify/instrumentation.ts @@ -20,19 +20,16 @@ import type { Span } from '@sentry/core'; import { isObjectLike, debug, - getActiveSpan, getIsolationScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, - spanToJSON, startInactiveSpan, startSpan, withActiveSpan, } from '@sentry/core'; import type { FastifyInstance, FastifyRequest } from './types'; import { DEBUG_BUILD } from '../../../debug-build'; +import { setHttpServerSpanRouteAttribute } from '../../../utils/setHttpServerSpanRouteAttribute'; const PACKAGE_NAME = '@sentry/instrumentation-fastify'; const SUPPORTED_VERSIONS = '>=3.21.0 <6'; @@ -187,15 +184,14 @@ function startRequestSpanHook(this: any, request: any, _reply: any, hookDone: () if (route != null) { attributes[HTTP_ROUTE] = route; - // Update the route of the request on the root span, if it is a http.server span - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan && getRootSpan(activeSpan); - if (rootSpan && spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'http.server') { - rootSpan.setAttribute(HTTP_ROUTE, route); - } + setHttpServerSpanRouteAttribute(route); } - const requestSpan = startInactiveSpan({ name: 'request', op: REQUEST_HANDLER_OP, attributes }); + const requestSpan = startInactiveSpan({ + name: route != null ? `${request.method} ${route}` : 'request', + op: REQUEST_HANDLER_OP, + attributes, + }); request[kRequestSpan] = requestSpan; // Set the request span as the active span for the remainder of the request lifecycle, so that diff --git a/packages/server-utils/src/integrations/tracing-channel/firebase/functions.ts b/packages/server-utils/src/integrations/tracing-channel/firebase/functions.ts index f50f71a445c3..07501358bfca 100644 --- a/packages/server-utils/src/integrations/tracing-channel/firebase/functions.ts +++ b/packages/server-utils/src/integrations/tracing-channel/firebase/functions.ts @@ -4,6 +4,7 @@ import { captureException, flush, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, startSpanManual, } from '@sentry/core'; @@ -72,7 +73,10 @@ function wrapHandler(handler: Handler, triggerType: string): Handler { { name: `firebase.function.${triggerType}`, op: 'function.firebase', - attributes, + attributes: { + ...attributes, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }, async span => { try { diff --git a/packages/server-utils/src/integrations/tracing-channel/hapi-utils.ts b/packages/server-utils/src/integrations/tracing-channel/hapi-utils.ts index 82af2a11f196..67c1ac0e1a45 100644 --- a/packages/server-utils/src/integrations/tracing-channel/hapi-utils.ts +++ b/packages/server-utils/src/integrations/tracing-channel/hapi-utils.ts @@ -7,14 +7,7 @@ * guard is replaced with `getActiveSpan()`. */ -import { - getActiveSpan, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, - startSpan, -} from '@sentry/core'; +import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; import type { LifecycleMethod, PatchableExtMethod, @@ -29,6 +22,7 @@ import type { // eslint-disable-next-line typescript/no-deprecated -- TODO(v11): Replace deprecated attributes import { HTTP_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes'; import { AttributeNames, handlerPatched, HapiLayerType, HapiLifecycleMethodNames } from './hapi-types'; +import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; type SpanAttributes = Record; @@ -37,28 +31,6 @@ interface SpanMetadata { name: string; } -/** - * Set the `http.route` attribute on the root HTTP server span for the current trace. - * - * No-op when there is no active span, no root span, or the root span is not an - * `http.server` span — so framework instrumentations can call this unconditionally - * without risking attribute pollution on non-HTTP root spans. - */ -function setHttpServerSpanRouteAttribute(route: string): void { - const activeSpan = getActiveSpan(); - if (!activeSpan) { - return; - } - const rootSpan = getRootSpan(activeSpan); - if (!rootSpan) { - return; - } - if (spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { - return; - } - rootSpan.setAttribute(HTTP_ROUTE, route); -} - const isLifecycleExtType = (variableToCheck: unknown): variableToCheck is ServerRequestExtType => { return typeof variableToCheck === 'string' && HapiLifecycleMethodNames.has(variableToCheck); }; @@ -91,17 +63,14 @@ export const getRouteMetadata = (route: ServerRoute, pluginName?: string): SpanM [HTTP_METHOD]: route.method, }; - let name; if (pluginName) { attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.PLUGIN; attributes[AttributeNames.PLUGIN_NAME] = pluginName; - name = `${pluginName}: route - ${route.path}`; } else { attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER; - name = `route - ${route.path}`; } - return { attributes, name }; + return { attributes, name: `${route.method.toUpperCase()} ${route.path}` }; }; /** Build the span name and attributes for a Hapi server extension. */ diff --git a/packages/server-utils/src/integrations/tracing-channel/knex.ts b/packages/server-utils/src/integrations/tracing-channel/knex.ts index f6a6fa9be7b7..870f63d56740 100644 --- a/packages/server-utils/src/integrations/tracing-channel/knex.ts +++ b/packages/server-utils/src/integrations/tracing-channel/knex.ts @@ -166,6 +166,7 @@ function subscribeQuery(): void { const name = connection?.filename || connection?.database || extractDatabaseFromConnectionString(connectionString); + const dbStatement = query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined; const attributes: SpanAttributes = { [SENTRY_KIND]: 'client', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, @@ -178,11 +179,11 @@ function subscribeQuery(): void { [NET_PEER_NAME]: connection?.host ?? extractHostFromConnectionString(connectionString), [NET_PEER_PORT]: connection?.port ?? extractPortFromConnectionString(connectionString), [NET_TRANSPORT]: connection?.filename === ':memory:' ? 'inproc' : undefined, - [DB_STATEMENT]: query?.sql != null ? truncate(query.sql, MAX_QUERY_LENGTH) : undefined, + [DB_STATEMENT]: dbStatement, }; return startInactiveSpan({ - name: getName(name, operation, table) ?? 'knex.query', + name: dbStatement ?? getName(name, operation, table) ?? 'knex.query', op: 'db', parentSpan, attributes, diff --git a/packages/server-utils/src/integrations/tracing-channel/koa.ts b/packages/server-utils/src/integrations/tracing-channel/koa.ts index 37ef6a0bd2fd..c89104d56105 100644 --- a/packages/server-utils/src/integrations/tracing-channel/koa.ts +++ b/packages/server-utils/src/integrations/tracing-channel/koa.ts @@ -6,16 +6,14 @@ import { getActiveSpan, getDefaultIsolationScope, getIsolationScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, startSpan, } from '@sentry/core'; // oxlint-disable-next-line typescript/no-deprecated import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_NAME, KOA_TYPE } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; +import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; // Same name as the OTel integration. When enabled, the OTel 'Koa' integration is omitted from the default set. const INTEGRATION_NAME = 'Koa' as const; @@ -231,28 +229,6 @@ function getMiddlewareMetadata( }; } -/** - * Set the `http.route` attribute on the root HTTP server span for the current trace. - * - * No-op when there is no active span, no root span, or the root span is not an - * `http.server` span — so this can be called unconditionally without risking - * attribute pollution on non-HTTP root spans. - */ -function setHttpServerSpanRouteAttribute(route: string): void { - const activeSpan = getActiveSpan(); - if (!activeSpan) { - return; - } - const rootSpan = getRootSpan(activeSpan); - if (!rootSpan) { - return; - } - if (spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { - return; - } - rootSpan.setAttribute(HTTP_ROUTE, route); -} - /** * Orchestrion-driven koa integration. Subscribes to the * `orchestrion:koa:use` channel injected into `Application.prototype.use` and diff --git a/packages/server-utils/src/integrations/tracing-channel/redis.ts b/packages/server-utils/src/integrations/tracing-channel/redis.ts index 755254631f3a..3305fb37f779 100644 --- a/packages/server-utils/src/integrations/tracing-channel/redis.ts +++ b/packages/server-utils/src/integrations/tracing-channel/redis.ts @@ -143,13 +143,14 @@ function nodeRedisAttributes(options: NodeRedisClientOptions | undefined): SpanA } function startCommandSpan(commandName: string, commandArgs: Array, attributes: SpanAttributes): Span { + const dbStatement = defaultDbStatementSerializer(commandName, commandArgs); return startInactiveSpan({ - name: `redis-${commandName}`, + name: dbStatement || `redis-${commandName}`, attributes: { [SENTRY_KIND]: 'client', ...attributes, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', - [DB_STATEMENT]: defaultDbStatementSerializer(commandName, commandArgs), + [DB_STATEMENT]: dbStatement, }, }); } diff --git a/packages/server-utils/src/integrations/tracing-channel/tedious.ts b/packages/server-utils/src/integrations/tracing-channel/tedious.ts index 45206f14c7ad..76def3062b50 100644 --- a/packages/server-utils/src/integrations/tracing-channel/tedious.ts +++ b/packages/server-utils/src/integrations/tracing-channel/tedious.ts @@ -142,7 +142,7 @@ function subscribeQuery(channelName: string, operation: string): void { }; const span = startInactiveSpan({ - name: getSpanName(operation, databaseName, sql, request.table), + name: sql || getSpanName(operation, databaseName, sql, request.table), op: 'db', attributes, }); diff --git a/packages/server-utils/src/mongodb/mongodb-span.ts b/packages/server-utils/src/mongodb/mongodb-span.ts index e0d2c81af579..ac4a242128de 100644 --- a/packages/server-utils/src/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/mongodb/mongodb-span.ts @@ -1,19 +1,23 @@ -import { SENTRY_KIND } from '@sentry/conventions/attributes'; +// The `@sentry/conventions` db/net attribute keys are deprecated (superseded by newer semconv), but we +// emit them deliberately to preserve parity with what `@opentelemetry/instrumentation-mongodb` produced. +/* oxlint-disable typescript/no-deprecated */ + +import { + DB_NAME, + DB_OPERATION, + DB_STATEMENT, + DB_SYSTEM, + NET_PEER_NAME, + NET_PEER_PORT, + SENTRY_KIND, +} from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -// OTel "OLD" db/net semantic-conventions, reproduced from the vendored -// `@opentelemetry/instrumentation-mongodb` span shape so the orchestrion -// spans match the OTel ones byte-for-byte. Inlined as literals to avoid -// importing the deprecated convention constants. -const ATTR_DB_SYSTEM = 'db.system'; -const ATTR_DB_NAME = 'db.name'; -const ATTR_DB_OPERATION = 'db.operation'; -const ATTR_DB_STATEMENT = 'db.statement'; +// OTel db/net keys/values not exported by `@sentry/conventions`, inlined to match +// what `@opentelemetry/instrumentation-mongodb` emitted. const ATTR_DB_MONGODB_COLLECTION = 'db.mongodb.collection'; const ATTR_DB_CONNECTION_STRING = 'db.connection_string'; -const ATTR_NET_PEER_NAME = 'net.peer.name'; -const ATTR_NET_PEER_PORT = 'net.peer.port'; const DB_SYSTEM_VALUE_MONGODB = 'mongodb'; /** @@ -120,24 +124,24 @@ export function getSpanAttributes( ): SpanAttributes { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MONGODB, - [ATTR_DB_NAME]: dbName, + [DB_SYSTEM]: DB_SYSTEM_VALUE_MONGODB, + [DB_NAME]: dbName, [ATTR_DB_MONGODB_COLLECTION]: dbCollection, - [ATTR_DB_OPERATION]: operation, + [DB_OPERATION]: operation, [ATTR_DB_CONNECTION_STRING]: `mongodb://${host}:${port}/${dbName}`, }; if (host && port) { - attributes[ATTR_NET_PEER_NAME] = host; + attributes[NET_PEER_NAME] = host; const portNumber = parseInt(port, 10); if (!isNaN(portNumber)) { - attributes[ATTR_NET_PEER_PORT] = portNumber; + attributes[NET_PEER_PORT] = portNumber; } } if (commandObj) { try { - attributes[ATTR_DB_STATEMENT] = serializeDbStatement(commandObj); + attributes[DB_STATEMENT] = serializeDbStatement(commandObj); } catch { // ignore serialization errors — the statement is best-effort metadata } @@ -222,7 +226,7 @@ export function getV3SpanAttributes( */ export function startMongoSpan(attributes: SpanAttributes): Span { return startInactiveSpan({ - name: `mongodb.${attributes[ATTR_DB_OPERATION] || 'command'}`, + name: (attributes[DB_STATEMENT] as string) || `mongodb.${attributes[DB_OPERATION] || 'command'}`, op: 'db', attributes: { [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/prisma/tracing-helper.ts b/packages/server-utils/src/prisma/tracing-helper.ts index 760762545d68..27ff4b96196f 100644 --- a/packages/server-utils/src/prisma/tracing-helper.ts +++ b/packages/server-utils/src/prisma/tracing-helper.ts @@ -24,7 +24,7 @@ import { } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import type { EngineSpan, ExtendedSpanOptions, SpanCallback, TracingHelper } from './types'; -import { DB_SYSTEM, SENTRY_KIND } from '@sentry/conventions/attributes'; +import { DB_STATEMENT, DB_SYSTEM, SENTRY_KIND } from '@sentry/conventions/attributes'; // Reading `process.env` can throw in runtimes that gate env access (e.g. Deno without `--allow-env`) // and `process` may be absent altogether (edge runtimes), so this degrades to `false` in those cases. @@ -104,6 +104,11 @@ function buildSpanAttributes(name: string, attributes: Record | * engine name. v5/v6 emit `prisma:engine:db_query`; v7 inlined the engine and emits `prisma:client:db_query`. */ function buildSpanName(name: string, attributes: SpanAttributes): string { + // oxlint-disable-next-line typescript/no-deprecated + const dbStatement = attributes[DB_STATEMENT]; + if (typeof dbStatement === 'string' && dbStatement) { + return dbStatement; + } const queryText = attributes['db.query.text']; if ((name === 'prisma:engine:db_query' || name === 'prisma:client:db_query') && typeof queryText === 'string') { return queryText; diff --git a/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts b/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts new file mode 100644 index 000000000000..0d7cf7273e29 --- /dev/null +++ b/packages/server-utils/src/utils/setHttpServerSpanRouteAttribute.ts @@ -0,0 +1,37 @@ +import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE } from '@sentry/conventions/attributes'; +import { + getActiveSpan, + getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, +} from '@sentry/core'; + +/** + * Set the `http.route` attribute on the root HTTP server span for the current trace. + * + * No-op when there is no active span, no root span, or the root span is not an + * `http.server` span — so framework instrumentations can call this unconditionally + * without risking attribute pollution on non-HTTP root spans. + */ +export function setHttpServerSpanRouteAttribute(route: string): void { + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + const rootSpan = getRootSpan(activeSpan); + if (!rootSpan) { + return; + } + + const attributes = spanToJSON(rootSpan).data; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') { + return; + } + + // eslint-disable-next-line typescript/no-deprecated + const method = attributes[HTTP_REQUEST_METHOD] || attributes[HTTP_METHOD] || 'GET'; + rootSpan.setAttribute(HTTP_ROUTE, route); + rootSpan.updateName(`${method} ${route}`); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); +} diff --git a/packages/server-utils/test/integrations/tracing-channel/hapi-utils.test.ts b/packages/server-utils/test/integrations/tracing-channel/hapi-utils.test.ts index 862e9ee3e747..e6c249f80a85 100644 --- a/packages/server-utils/test/integrations/tracing-channel/hapi-utils.test.ts +++ b/packages/server-utils/test/integrations/tracing-channel/hapi-utils.test.ts @@ -6,7 +6,7 @@ describe('getRouteMetadata', () => { it('describes a directly-registered route as a router layer', () => { expect(getRouteMetadata(route)).toEqual({ - name: 'route - /users/{id}', + name: 'GET /users/{id}', attributes: { 'http.route': '/users/{id}', 'http.method': 'get', @@ -17,7 +17,7 @@ describe('getRouteMetadata', () => { it('describes a plugin-registered route as a plugin layer', () => { expect(getRouteMetadata(route, 'my-plugin')).toEqual({ - name: 'my-plugin: route - /users/{id}', + name: 'GET /users/{id}', attributes: { 'http.route': '/users/{id}', 'http.method': 'get', diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 49e189b29f88..dc2bb906c783 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -69,7 +69,9 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/solid": "10.67.0", - "@sentry/bundler-plugins": "10.67.0" + "@sentry/bundler-plugins": "10.67.0", + "@sentry/server-utils": "10.67.0", + "@sentry/conventions": "0.16.0" }, "devDependencies": { "@solidjs/router": "^0.15.0", diff --git a/packages/solidstart/src/server/withServerActionInstrumentation.ts b/packages/solidstart/src/server/withServerActionInstrumentation.ts index bcd9389a6bf4..985a44dcc6d0 100644 --- a/packages/solidstart/src/server/withServerActionInstrumentation.ts +++ b/packages/solidstart/src/server/withServerActionInstrumentation.ts @@ -6,6 +6,8 @@ import { } from '@sentry/core'; import { captureException, getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, startSpan } from '@sentry/node'; import { isRedirect } from './utils'; +import { HTTP_ROUTE, HTTP_TARGET } from '@sentry/conventions/attributes'; +import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; /** * Wraps a server action (functions that use the 'use server' directive) @@ -25,9 +27,9 @@ export async function withServerActionInstrumentation ''); const mockFlush = vi.spyOn(SentryCore, 'flushIfServerless').mockImplementation(async () => {}); -const mockGetActiveSpan = vi.spyOn(SentryNode, 'getActiveSpan'); +const mockGetActiveSpan = vi.spyOn(SentryCore, 'getActiveSpan'); const mockGetRequestEvent = vi.fn(); vi.mock('solid-js/web', async () => { @@ -128,8 +128,12 @@ describe('withServerActionInstrumentation', () => { }); it('sets a server action name on the active span', async () => { - const span = new SentryCore.SentrySpan(); - span.setAttribute('http.target', '/_server'); + const span = new SentryCore.SentrySpan({ + attributes: { + 'http.target': '/_server', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + }, + }); mockGetActiveSpan.mockReturnValue(span); const mockSpanSetAttribute = vi.spyOn(span, 'setAttribute'); @@ -141,9 +145,9 @@ describe('withServerActionInstrumentation', () => { await getPrefecture(); - expect(mockGetActiveSpan).to.toHaveBeenCalledTimes(1); + expect(mockGetActiveSpan).to.toHaveBeenCalledTimes(2); expect(mockSpanSetAttribute).to.toHaveBeenCalledWith('http.route', 'getPrefecture'); - expect(mockSpanSetAttribute).to.toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component'); + expect(mockSpanSetAttribute).to.toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('does not set a server action name if the active span had a non `/_server` target', async () => { From 9994c93476fe0d6e95fa54cbad81d706e3607e57 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 28 Jul 2026 14:27:38 +0200 Subject: [PATCH 2/2] fixes --- .../nitro-3/tests/isolation.test.ts | 1 - .../test/runtime/hooks/captureErrorHook.test.ts | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts index ce5159889f78..7234fa0948ca 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/isolation.test.ts @@ -3,7 +3,6 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Isolation scope prevents tag leaking between requests', async ({ request }) => { const transactionEventPromise = waitForTransaction('nitro-3', event => { - console.log('event', event.transaction, event.contexts?.trace); return event?.transaction === 'GET /api/test-isolation/:id'; }); diff --git a/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts index 804ef569a619..1041e2725527 100644 --- a/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts +++ b/packages/nitro/test/runtime/hooks/captureErrorHook.test.ts @@ -61,19 +61,6 @@ describe('captureErrorHook', () => { ); }); - it('should set transaction name from method and path', async () => { - const mockSetTransactionName = vi.fn(); - (SentryCore.getCurrentScope as any).mockReturnValue({ - setTransactionName: mockSetTransactionName, - }); - - const error = new Error('Test error'); - - await captureErrorHook(error, mockErrorContext); - - expect(mockSetTransactionName).toHaveBeenCalledWith('GET /test-path'); - }); - it('should skip HTTPError with 4xx status codes', async () => { const error = new HTTPError({ status: 404, message: 'Not found' });