From 6ae37e8299f1763e4f84ef4ddc5d8d1f1fb2af4a Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 22 Jul 2026 18:54:57 +0200 Subject: [PATCH 1/4] feat: Add `url.full` and `url.path` attributes to `http.server` spans (#22496) First part of https://github.com/getsentry/sentry-javascript/issues/22417: This PR adds the `url.full` and `url.path` attributes to all instrumentation that starts or takes over `http.server` spans. To ensure we get a good span description inferred for streamed spans, we need to add the respective attributes to these spans. In a second PR, I'll add `url.template`/`http.route` attributes which should only be set on spans when we have a parameterized route. ref https://github.com/getsentry/sentry-javascript/issues/22417 --------- Co-authored-by: GPT-5.6 Sol --- .../nestjs-11/tests/transactions.test.ts | 2 ++ .../nestjs-8/tests/transactions.test.ts | 2 ++ .../nestjs-basic/tests/transactions.test.ts | 2 ++ .../tests/propagation.test.ts | 8 ++++++++ .../tests/transactions.test.ts | 2 ++ .../tests/transactions.test.ts | 2 ++ .../tests/transactions.test.ts | 2 ++ .../tests/transactions.test.ts | 2 ++ .../node-express-v5/tests/transactions.test.ts | 2 ++ .../node-express/tests/transactions.test.ts | 2 ++ .../node-fastify-3/tests/propagation.test.ts | 8 ++++++++ .../node-fastify-3/tests/transactions.test.ts | 2 ++ .../node-fastify-4/tests/propagation.test.ts | 8 ++++++++ .../node-fastify-4/tests/transactions.test.ts | 2 ++ .../node-fastify-5/tests/propagation.test.ts | 8 ++++++++ .../node-fastify-5/tests/transactions.test.ts | 2 ++ .../node-hapi/tests/transactions.test.ts | 2 ++ .../node-koa/tests/propagation.test.ts | 8 ++++++++ .../node-koa/tests/transactions.test.ts | 2 ++ .../tests/sampling.test.ts | 2 ++ .../tests/transactions.test.ts | 2 ++ .../node-otel/tests/transactions.test.ts | 2 ++ .../tests/tracing.server.test.ts | 2 ++ .../tsx-express/tests/transactions.test.ts | 2 ++ .../suites/tracing/httpIntegration/test.ts | 5 +++++ packages/astro/src/server/middleware.ts | 3 +++ packages/astro/test/server/middleware.test.ts | 5 +++++ .../integrations/http/server-subscription.ts | 7 ++++++- .../http/server-subscription.test.ts | 3 +++ packages/elysia/package.json | 3 ++- packages/elysia/src/withElysia.ts | 3 +++ .../wrapApiHandlerWithSentry.ts | 8 ++++++++ .../src/edge/wrapApiHandlerWithSentry.ts | 15 +++++++++++++++ packages/nextjs/test/config/withSentry.test.ts | 18 ++++++++++++++---- .../http/httpServerSpansIntegration.ts | 4 ++++ .../src/server/createServerInstrumentation.ts | 2 ++ .../server/createServerInstrumentation.test.ts | 3 +++ packages/remix/src/server/instrumentServer.ts | 5 ++++- .../src/server/integrations/tracing-channel.ts | 15 ++++++++++++++- packages/sveltekit/src/server-common/handle.ts | 6 ++++++ 40 files changed, 175 insertions(+), 8 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts index 375c56a845d6..31fd0c8f6970 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts index 7270ad211909..23a11f67b0a3 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts index d56ddf007e9c..d6bec81e67db 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts @@ -49,6 +49,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts index 0a23c1766b38..1da006fca893 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts @@ -64,6 +64,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.path': `/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -103,6 +105,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -193,6 +197,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.path': `/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -232,6 +238,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts index 9ca18ec0888f..344d2440a9da 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction from module', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/example-module/transaction', + 'url.full': 'http://localhost:3030/example-module/transaction', + 'url.path': '/example-module/transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts index ddfcb1192edf..b0b9e71a4bfe 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction from module', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/example-module/transaction', + 'url.full': 'http://localhost:3030/example-module/transaction', + 'url.path': '/example-module/transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-express-orchestrion-cjs/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-orchestrion-cjs/tests/transactions.test.ts index 9dbce2a05ac9..1b9d488958c7 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-orchestrion-cjs/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-orchestrion-cjs/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts index fb11235943b2..cf1790853c86 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-orchestrion/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts index ba9632aaf952..c44da1ed290f 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts index c0c286da3345..5d995d844f93 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts index 1cdfd67a4851..41028122b492 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts @@ -64,6 +64,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.path': `/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -103,6 +105,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -193,6 +197,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.path': `/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -232,6 +238,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts index 6c53f21bd869..22b12c322169 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts @@ -28,6 +28,8 @@ test.skip('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts index 6e6b20b916e8..4b3e79b8b21d 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts @@ -64,6 +64,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.path': `/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -103,6 +105,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -193,6 +197,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.path': `/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -232,6 +238,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts index b9a41cd4e572..7209031eb53c 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts index 4e903edf05b5..c7f833701f52 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts @@ -64,6 +64,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.path': `/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -103,6 +105,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -193,6 +197,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.path': `/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -232,6 +238,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts index b4460cde2a21..f90bcf06b717 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts index bd6540b088d3..bfd71c2be730 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts @@ -22,6 +22,8 @@ test('Sends successful transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-success', + 'url.full': 'http://localhost:3030/test-success', + 'url.path': '/test-success', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts index 592c5a4717f4..dcb952069bef 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts @@ -63,6 +63,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-http/${id}`, + 'url.path': `/test-outgoing-http/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -102,6 +104,8 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -192,6 +196,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.full': `http://localhost:3030/test-outgoing-fetch/${id}`, + 'url.path': `/test-outgoing-fetch/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', @@ -231,6 +237,8 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.full': `http://localhost:3030/test-inbound-headers/${id}`, + 'url.path': `/test-inbound-headers/${id}`, 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts index f86901e0dee4..8952ec88a8ae 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts index 49d35cb9e85f..12753312cdb2 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts @@ -21,6 +21,8 @@ test('Sends a sampled API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/task', + 'url.full': 'http://localhost:3030/task', + 'url.path': '/task', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts index 299d3c2b80ec..b128a537b856 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts @@ -35,6 +35,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts index ba77e6a3b294..b77c0a610512 100644 --- a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts @@ -35,6 +35,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts index c6de70d0e6a1..9c2668a7c6c8 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts @@ -82,6 +82,8 @@ test('server pageload request span has nested request span for sub request', asy 'http.method': 'GET', 'http.route': '/api/users', 'http.url': 'http://localhost:3030/api/users', + 'url.full': 'http://localhost:3030/api/users', + 'url.path': '/api/users', 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.sveltekit', 'sentry.source': 'route', diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts index 35fe8f17bd94..c76c7653d30f 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts @@ -23,6 +23,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index ac0ac3780a38..69740b8bdaf4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -1,4 +1,5 @@ import { createTestServer } from '@sentry-internal/test-utils'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests, createRunner } from '../../../utils/runner'; @@ -130,6 +131,8 @@ describe('httpIntegration', () => { 'sentry.sample_rate': 1, 'sentry.source': 'route', url: `http://localhost:${port}/test`, + [URL_FULL]: `http://localhost:${port}/test?a=1&b=2`, + [URL_PATH]: '/test', ...getCommonHttpRequestHeaders(), }); }, @@ -172,6 +175,8 @@ describe('httpIntegration', () => { 'sentry.sample_rate': 1, 'sentry.source': 'route', url: `http://localhost:${port}/test`, + [URL_FULL]: `http://localhost:${port}/test?a=1&b=2`, + [URL_PATH]: '/test', 'http.request.header.content_length': '9', 'http.request.header.content_type': 'text/plain;charset=UTF-8', ...getCommonHttpRequestHeaders(), diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index fc606654e389..5631338bd63b 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; import { addNonEnumerableProperty, @@ -218,6 +219,8 @@ async function instrumentRequestStartHttpServerSpan( [SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: method, // This is here for backwards compatibility, we used to set this here before method, + [URL_FULL]: ctx.url.href, + [URL_PATH]: ctx.url.pathname, url: stripUrlQueryAndFragment(ctx.url.href), ...httpHeadersToSpanAttributes( winterCGHeadersToDict(request.headers), diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 205cfb7e757f..9f051ebd703e 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import type { Client, Span } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryCore from '@sentry/core'; @@ -117,6 +118,8 @@ describe('sentryMiddleware', () => { 'sentry.origin': 'auto.http.astro', method: 'GET', url: 'https://mydomain.io/users/123/details', + [URL_FULL]: 'https://mydomain.io/users/123/details', + [URL_PATH]: '/users/123/details', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SentryCore.SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: 'GET', 'http.route': '/users/[id]/details', @@ -154,6 +157,8 @@ describe('sentryMiddleware', () => { 'sentry.origin': 'auto.http.astro', method: 'GET', url: 'http://localhost:1234/a%xx', + [URL_FULL]: 'http://localhost:1234/a%xx', + [URL_PATH]: 'a%xx', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', [SentryCore.SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD]: 'GET', }, diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index 86d7359aa119..7517b88509c9 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -1,3 +1,4 @@ +// oxlint-disable max-lines /** * Provide the `http.server.request.start` subscription function that we use * to instrument incoming HTTP requests that use the `node:http` module. @@ -40,6 +41,7 @@ import { safeMathRandom } from '../../utils/randomSafeContext'; import { SPAN_KIND } from '../../spanKind'; import type { SpanAttributes } from '../../types/span'; import type { SpanStatus } from '../../types/spanStatus'; +import { HTTP_URL, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; @@ -298,7 +300,10 @@ function buildServerSpanWrap( 'net.peer.port': remotePort, 'sentry.http.prefetch': isKnownPrefetchRequest(request) || undefined, // Old Semantic Conventions attributes for compatibility - 'http.url': fullUrl, + [URL_FULL]: fullUrl, + [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, + // oxlint-disable-next-line typescript-eslint(no-deprecated) + [HTTP_URL]: fullUrl, 'http.method': method, 'http.target': urlObj ? `${urlObj.pathname}${urlObj.search}` : httpTargetWithoutQueryFragment, 'http.host': host, 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 6be81c51f210..8c1fc3594403 100644 --- a/packages/core/test/lib/integrations/http/server-subscription.test.ts +++ b/packages/core/test/lib/integrations/http/server-subscription.test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import * as http from 'node:http'; import type { AddressInfo } from 'node:net'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -107,6 +108,8 @@ describe('getHttpServerSubscriptions', () => { 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.server', 'sentry.source': 'url', + [URL_FULL]: expect.stringMatching(/\/users\/42\?foo=bar$/), + [URL_PATH]: '/users/42', }), }), ); diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 7ad247aa325b..357b4a8188cb 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -40,7 +40,8 @@ }, "dependencies": { "@sentry/bun": "10.67.0", - "@sentry/core": "10.67.0" + "@sentry/core": "10.67.0", + "@sentry/conventions": "^0.16.0" }, "peerDependencies": { "elysia": "^1.4.0" diff --git a/packages/elysia/src/withElysia.ts b/packages/elysia/src/withElysia.ts index 84a496300d01..9deaf3706c19 100644 --- a/packages/elysia/src/withElysia.ts +++ b/packages/elysia/src/withElysia.ts @@ -1,3 +1,4 @@ +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; import { captureException, @@ -198,6 +199,8 @@ export function withElysia(app: T, options: ElysiaHandlerOp attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: request.url, + [URL_PATH]: new URL(request.url).pathname, }, }, rootSpan => { diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 60a9b0d617f7..406965ee696a 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -5,7 +5,9 @@ import { getActiveSpan, httpRequestToRequestData, isString, + isURLObjectRelative, objectify, + parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus, @@ -16,6 +18,7 @@ import type { NextApiRequest } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; export type AugmentedNextApiRequest = NextApiRequest & { __withSentry_applied__?: boolean; @@ -78,6 +81,9 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz isolationScope.setSDKProcessingMetadata({ normalizedRequest }); isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`); + const requestUrl = normalizedRequest.url || req.url; + const urlObject = requestUrl ? parseStringToURLObject(requestUrl) : undefined; + return startSpanManual( { name: `${reqMethod}${parameterizedRoute}`, @@ -86,6 +92,8 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', + [URL_FULL]: urlObject && !isURLObjectRelative(urlObject) ? urlObject.href : undefined, + [URL_PATH]: urlObject?.pathname, }, }, async span => { diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 528c174e45fa..1aac5499130f 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -4,10 +4,13 @@ import { getCurrentScope, getRootSpan, handleCallbackErrors, + isURLObjectRelative, + parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCapturedScopesOnSpan, + spanToJSON, startSpan, winterCGRequestToRequestData, withIsolationScope, @@ -15,6 +18,7 @@ import { import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from './types'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; /** * Wraps a Next.js edge route handler with Sentry error and performance instrumentation. @@ -48,6 +52,13 @@ export function wrapApiHandlerWithSentry( // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can // rely on that for parameterization. + const urlObject = req instanceof Request ? parseStringToURLObject(req.url) : undefined; + + const urlAttributes = { + [URL_FULL]: urlObject && !isURLObjectRelative(urlObject) ? urlObject.href : undefined, + [URL_PATH]: urlObject?.pathname, + }; + const activeSpan = getActiveSpan(); if (activeSpan) { spanName = `handler (${parameterizedRoute})`; @@ -55,12 +66,15 @@ export function wrapApiHandlerWithSentry( const rootSpan = getRootSpan(activeSpan); if (rootSpan) { + const rootSpanAttributes = spanToJSON(rootSpan).data; rootSpan.updateName( req instanceof Request ? `${req.method} ${parameterizedRoute}` : `handler ${parameterizedRoute}`, ); rootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [URL_FULL]: rootSpanAttributes[URL_FULL] ?? urlAttributes[URL_FULL], + [URL_PATH]: rootSpanAttributes[URL_PATH] ?? urlAttributes[URL_PATH], ...headerAttributes, }); setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); @@ -78,6 +92,7 @@ export function wrapApiHandlerWithSentry( attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_api_handler', + ...urlAttributes, ...headerAttributes, }, }, diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index 5b6643358f57..bcc19a1ef568 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,4 +1,5 @@ import * as SentryCore from '@sentry/core'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { NextApiRequest, NextApiResponse } from 'next'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -17,7 +18,13 @@ describe('withSentry', () => { const wrappedHandlerNoError = wrapApiHandlerWithSentry(origHandlerNoError, '/my-parameterized-route'); beforeEach(() => { - req = { url: 'http://dogs.are.great' } as NextApiRequest; + req = { + headers: { + host: 'dogs.are.great', + 'x-forwarded-proto': 'https', + }, + url: '/api/dogs?good=true', + } as NextApiRequest; res = { send: function (this: AugmentedNextApiResponse) { this.end(); @@ -36,17 +43,20 @@ describe('withSentry', () => { }); describe('tracing', () => { - it('starts a transaction when tracing is enabled', async () => { + it('starts a transaction with normalized request URL attributes', async () => { await wrappedHandlerNoError(req, res); expect(startSpanManualSpy).toHaveBeenCalledWith( - expect.objectContaining({ + { name: 'GET /my-parameterized-route', op: 'http.server', + forceTransaction: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs', + [URL_FULL]: 'https://dogs.are.great/api/dogs?good=true', + [URL_PATH]: '/api/dogs', }, - }), + }, expect.any(Function), ); }); diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index 33e02c6cc9e9..a09a1759fd84 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -23,6 +23,8 @@ import { NET_PEER_PORT, NET_TRANSPORT, SENTRY_HTTP_PREFETCH, + URL_FULL, + URL_PATH, } from '@sentry/conventions/attributes'; import type { Event, @@ -172,6 +174,8 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, + [URL_FULL]: fullUrl, + [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, // Old Semantic Conventions attributes - added for compatibility with what `@opentelemetry/instrumentation-http` output before /* eslint-disable typescript/no-deprecated */ [HTTP_URL]: fullUrl, diff --git a/packages/react-router/src/server/createServerInstrumentation.ts b/packages/react-router/src/server/createServerInstrumentation.ts index 982345db691d..be826c3d7f5d 100644 --- a/packages/react-router/src/server/createServerInstrumentation.ts +++ b/packages/react-router/src/server/createServerInstrumentation.ts @@ -65,6 +65,8 @@ export function createSentryServerInstrumentation( [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [URL_FULL]: info.request.url, + [URL_PATH]: pathname, }); try { diff --git a/packages/react-router/test/server/createServerInstrumentation.test.ts b/packages/react-router/test/server/createServerInstrumentation.test.ts index 32152b5b6bc0..80b1a4597901 100644 --- a/packages/react-router/test/server/createServerInstrumentation.test.ts +++ b/packages/react-router/test/server/createServerInstrumentation.test.ts @@ -1,4 +1,5 @@ import * as otelApi from '@opentelemetry/api'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import * as core from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -116,6 +117,8 @@ describe('createSentryServerInstrumentation', () => { 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.react_router.instrumentation_api', 'sentry.source': 'url', + [URL_FULL]: 'http://example.com/test-path', + [URL_PATH]: '/test-path', }); expect(mockHandleRequest).toHaveBeenCalled(); expect(core.flushIfServerless).toHaveBeenCalled(); diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index a9003c925803..8c157d697687 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -40,6 +40,7 @@ import { createRoutes, getTransactionName, isCloudflareEnv } from '../utils/util import { extractData, isResponse, json } from '../utils/vendor/response'; import { captureRemixServerException, errorHandleDataFunction } from './errors'; import { generateSentryServerTimingHeader, injectServerTimingHeaderValue } from './serverTimingTracePropagation'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; type AppData = unknown; type RemixRequest = Parameters[0]; @@ -337,8 +338,8 @@ function wrapRequestHandler ServerBuild | Promise DEBUG_BUILD && debug.warn('Failed to normalize Remix request'); } + const url = new URL(request.url); if (options?.instrumentTracing && resolvedRoutes) { - const url = new URL(request.url); [name, source] = getTransactionName(resolvedRoutes, url); isolationScope.setTransactionName(name); @@ -375,6 +376,8 @@ function wrapRequestHandler ServerBuild | Promise [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [URL_FULL]: url.href, + [URL_PATH]: url.pathname, method: request.method, ...httpHeadersToSpanAttributes( winterCGHeadersToDict(request.headers), diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 624ab8dad39c..04d5e4a21269 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -3,6 +3,8 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { getActiveSpan, isObjectLike, + isURLObjectRelative, + parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_KIND, @@ -10,7 +12,15 @@ import { waitForTracingChannelBinding, } from '@sentry/core'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; -import { CODE_FUNCTION, HTTP_METHOD, HTTP_ROUTE, HTTP_STATUS_CODE, HTTP_URL } from '@sentry/conventions/attributes'; +import { + CODE_FUNCTION, + HTTP_METHOD, + HTTP_ROUTE, + HTTP_STATUS_CODE, + HTTP_URL, + URL_FULL, + URL_PATH, +} from '@sentry/conventions/attributes'; import { remixChannels } from '@sentry/server-utils/orchestrion'; const ORIGIN = 'auto.http.orchestrion.remix'; @@ -64,6 +74,9 @@ function getRequestAttributes(request: unknown): SpanAttributes { if (typeof url === 'string') { // oxlint-disable-next-line typescript/no-deprecated attributes[HTTP_URL] = url; + const urlObject = parseStringToURLObject(url); + attributes[URL_FULL] = urlObject && !isURLObjectRelative(urlObject) ? urlObject.href : undefined; + attributes[URL_PATH] = urlObject?.pathname; } return attributes; } diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 2e30253aaaf2..5ebdbf97d98b 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -23,6 +23,7 @@ import { import type { Handle, ResolveOptions } from '@sveltejs/kit'; import { DEBUG_BUILD } from '../common/debug-build'; import { getTracePropagationData, sendErrorToSentry } from './utils'; +import { HTTP_URL, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; export type SentryHandleOptions = { /** @@ -178,6 +179,9 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url', 'sveltekit.tracing.original_name': originalName, + // oxlint-disable-next-line typescript-eslint(no-deprecated) + [URL_FULL]: kitRootSpanAttributes[URL_FULL] ?? kitRootSpanAttributes[HTTP_URL] ?? event.url.href, + [URL_PATH]: kitRootSpanAttributes[URL_PATH] ?? event.url.pathname, ...httpHeadersToSpanAttributes( winterCGHeadersToDict(event.request.headers), getClient()?.getDataCollectionOptions() ?? false, @@ -207,6 +211,8 @@ async function instrumentHandle( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', 'http.method': event.request.method, + [URL_FULL]: event.url.href, + [URL_PATH]: event.url.pathname, ...httpHeadersToSpanAttributes( winterCGHeadersToDict(event.request.headers), getClient()?.getDataCollectionOptions() ?? false, From 3d13df01dca1a6d4a2b69bb84df879d596e23249 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 23 Jul 2026 12:12:08 +0200 Subject: [PATCH 2/4] test(v10): Update Connect URL attribute expectations Keep the exact Connect server span assertion aligned with the URL attributes added by the v10 backport. Refs #22496 Co-Authored-By: GPT-5.6 Sol Co-authored-by: Cursor --- .../test-applications/node-connect/tests/transactions.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts index f6991ed7a75a..de7e8671022e 100644 --- a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts @@ -24,6 +24,8 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'otel.kind': 'SERVER', 'http.response.status_code': 200, 'http.url': 'http://localhost:3030/test-transaction', + 'url.full': 'http://localhost:3030/test-transaction', + 'url.path': '/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', 'http.method': 'GET', From 770f7c297177624fa3a0f74428dfaa65972a0998 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 23 Jul 2026 13:17:18 +0200 Subject: [PATCH 3/4] fix(v10/core): Omit relative `url.full` attributes Only record `url.full` when the incoming request resolves to an absolute URL, while retaining `url.path` for relative requests without a host. Refs #22496 Co-Authored-By: GPT-5.6 Sol Co-authored-by: Cursor --- .../integrations/http/server-subscription.ts | 4 +-- .../http/server-subscription.test.ts | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index 7517b88509c9..1e93166c9de4 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -27,7 +27,7 @@ import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } fro import { hasSpansEnabled } from '../../utils/hasSpansEnabled'; import { headersToDict, httpHeadersToSpanAttributes, httpRequestToRequestData } from '../../utils/request'; import { patchRequestToCaptureBody } from './patch-request-to-capture-body'; -import { parseStringToURLObject, stripUrlQueryAndFragment } from '../../utils/url'; +import { isURLObjectRelative, parseStringToURLObject, stripUrlQueryAndFragment } from '../../utils/url'; import { recordRequestSession } from './record-request-session'; import { generateSpanId, generateTraceId } from '../../utils/propagationContext'; import { continueTrace } from '../../tracing/trace'; @@ -300,7 +300,7 @@ function buildServerSpanWrap( 'net.peer.port': remotePort, 'sentry.http.prefetch': isKnownPrefetchRequest(request) || undefined, // Old Semantic Conventions attributes for compatibility - [URL_FULL]: fullUrl, + [URL_FULL]: urlObj && !isURLObjectRelative(urlObj) ? urlObj.href : undefined, [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, // oxlint-disable-next-line typescript-eslint(no-deprecated) [HTTP_URL]: fullUrl, 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 8c1fc3594403..e5be2e7f4e9b 100644 --- a/packages/core/test/lib/integrations/http/server-subscription.test.ts +++ b/packages/core/test/lib/integrations/http/server-subscription.test.ts @@ -1,5 +1,6 @@ import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; import * as http from 'node:http'; +import * as net from 'node:net'; import type { AddressInfo } from 'node:net'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -68,6 +69,18 @@ describe('getHttpServerSubscriptions', () => { }); } + async function makeRequestWithoutHost(path: string): Promise { + const { port } = server.address() as AddressInfo; + return new Promise((resolve, reject) => { + const socket = net.createConnection(port, '127.0.0.1', () => { + socket.write(`GET ${path} HTTP/1.0\r\nConnection: close\r\n\r\n`); + }); + socket.on('data', () => undefined); + socket.on('end', resolve); + socket.on('error', reject); + }); + } + function instrument(spans: boolean, extra: { ignoreStaticAssets?: boolean } = {}): void { const { [HTTP_ON_SERVER_REQUEST]: onServerRequest } = getHttpServerSubscriptions({ spans, ...extra }); // Fire the channel listener manually with the server we're about to use. @@ -115,6 +128,22 @@ describe('getHttpServerSubscriptions', () => { ); }); + it('omits url.full when the incoming request URL is relative', async () => { + server = http.createServer((_req, res) => res.end('ok')); + await new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve())); + instrument(true); + + await makeRequestWithoutHost('/users/42?foo=bar'); + const transaction = await waitForTransaction(); + + expect(transaction.contexts?.trace?.data).toEqual( + expect.objectContaining({ + [URL_PATH]: '/users/42', + }), + ); + expect(transaction.contexts?.trace?.data).not.toHaveProperty(URL_FULL); + }); + it('reports a 500 status with internal_error span status', async () => { server = http.createServer((_req, res) => { res.statusCode = 500; From 3b3b0dcce18ad29a17c0cc1c9cc7083db3ead3bc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 23 Jul 2026 13:32:22 +0200 Subject: [PATCH 4/4] fix(v10/node-core): Omit relative `url.full` attributes Align Node server spans with the URL attribute contract by retaining `url.path` but omitting `url.full` when only a relative request URL exists. Refs #22496 Co-Authored-By: GPT-5.6 Sol Co-authored-by: Cursor --- .../http/httpServerSpansIntegration.ts | 3 +- .../httpServerSpansIntegration.test.ts | 48 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index a09a1759fd84..fdf350214f9b 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -42,6 +42,7 @@ import { getIsolationScope, getSpanStatusFromHttpCode, httpHeadersToSpanAttributes, + isURLObjectRelative, parseStringToURLObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -174,7 +175,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, - [URL_FULL]: fullUrl, + [URL_FULL]: urlObj && !isURLObjectRelative(urlObj) ? urlObj.href : undefined, [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, // Old Semantic Conventions attributes - added for compatibility with what `@opentelemetry/instrumentation-http` output before /* eslint-disable typescript/no-deprecated */ diff --git a/packages/node-core/test/integrations/httpServerSpansIntegration.test.ts b/packages/node-core/test/integrations/httpServerSpansIntegration.test.ts index f1b5af564d79..bb144d701544 100644 --- a/packages/node-core/test/integrations/httpServerSpansIntegration.test.ts +++ b/packages/node-core/test/integrations/httpServerSpansIntegration.test.ts @@ -1,4 +1,7 @@ -import { describe, expect, it } from 'vitest'; +import { EventEmitter } from 'node:events'; +import { URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import * as SentryCore from '@sentry/core'; +import { describe, expect, it, vi } from 'vitest'; import { httpServerSpansIntegration, isStaticAssetRequest, @@ -35,6 +38,49 @@ describe('httpIntegration', () => { }); }); + it('omits url.full when the incoming request URL is relative', () => { + let onHttpServerRequest: + | ((request: unknown, response: unknown, normalizedRequest: SentryCore.RequestEventData) => void) + | undefined; + const client = { + on: (hook: string, callback: typeof onHttpServerRequest) => { + if (hook === 'httpServerRequest') { + onHttpServerRequest = callback; + } + }, + getDataCollectionOptions: () => false, + }; + const span = { + end: () => undefined, + setAttributes: () => undefined, + setStatus: () => undefined, + } as unknown as SentryCore.Span; + const startInactiveSpan = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span); + const request = Object.assign(new EventEmitter(), { + headers: {}, + httpVersion: '1.0', + method: 'GET', + socket: {}, + url: '/users/42?foo=bar', + }) as EventEmitter & { + _startSpanCallback?: { deref: () => ((next: () => boolean) => boolean) | undefined }; + }; + const response = Object.assign(new EventEmitter(), { statusCode: 200 }); + + const integration = httpServerSpansIntegration(); + integration.setup?.(client as Parameters>[0]); + onHttpServerRequest?.(request, response, { headers: {}, method: 'GET' }); + request._startSpanCallback?.deref()(() => true); + + const attributes = startInactiveSpan.mock.calls[0]?.[0].attributes; + expect(attributes).toEqual( + expect.objectContaining({ + [URL_PATH]: '/users/42', + }), + ); + expect(attributes?.[URL_FULL]).toBeUndefined(); + }); + describe('processEvent', () => { function runProcessEvent(event: Record, options = {}): any { const integration = httpServerSpansIntegration(options);