From b96fd7f4b10fcb6db05a8159e29d6bd897424705 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 11:00:40 +0200 Subject: [PATCH 1/7] WIP: nextjs http.client spans --- .../app/route-handlers/[param]/edge/route.ts | 2 + .../nextjs-app-dir/tests/edge-route.test.ts | 61 -------- .../tests/pages-router-edge-route.test.ts | 130 ++++++++++++++++++ .../tests/route-handlers.test.ts | 57 ++++++++ .../SentryNodeFetchInstrumentation.ts | 6 +- .../node/src/integrations/node-fetch/index.ts | 8 ++ packages/node/src/utils/isNextEdgeRuntime.ts | 8 ++ yarn.lock | 2 +- 8 files changed, 211 insertions(+), 63 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-router-edge-route.test.ts create mode 100644 packages/node/src/utils/isNextEdgeRuntime.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts index 8879a85c488a..7d2ea2341a99 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts @@ -5,6 +5,8 @@ import { NextResponse } from 'next/server'; export const runtime = 'edge'; export async function PATCH() { + // Test that actual fetch requests are captured + await fetch('https://github.com'); return NextResponse.json({ name: 'John Doe' }, { status: 401 }); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts deleted file mode 100644 index 88460e3ab533..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; - -test('Should create a transaction for edge routes', async ({ request }) => { - const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'GET /api/edge-endpoint' && - transactionEvent.contexts?.runtime?.name === 'vercel-edge' - ); - }); - - const response = await request.get('/api/edge-endpoint', { - headers: { - 'x-yeet': 'test-value', - }, - }); - expect(await response.json()).toStrictEqual({ name: 'Jim Halpert' }); - - const edgerouteTransaction = await edgerouteTransactionPromise; - - expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); - expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value'); -}); - -test('Faulty edge routes', async ({ request }) => { - const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && - transactionEvent.contexts?.runtime?.name === 'vercel-edge' - ); - }); - - const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { - return ( - errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error' && - errorEvent.contexts?.runtime?.name === 'vercel-edge' - ); - }); - - request.get('/api/error-edge-endpoint').catch(() => { - // Noop - }); - - const [edgerouteTransaction, errorEvent] = await Promise.all([ - test.step('should create a transaction', () => edgerouteTransactionPromise), - test.step('should create an error event', () => errorEventPromise), - ]); - - test.step('should create transactions with the right fields', () => { - expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error'); - expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - }); - - test.step('should have scope isolation', () => { - expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); - expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); - expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-router-edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-router-edge-route.test.ts new file mode 100644 index 000000000000..9cccb1d9a6a6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-router-edge-route.test.ts @@ -0,0 +1,130 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should create a transaction for edge routes', async ({ request }) => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + const response = await request.get('/api/edge-endpoint', { + headers: { + 'x-yeet': 'test-value', + }, + }); + expect(await response.json()).toStrictEqual({ name: 'Jim Halpert' }); + + const edgerouteTransaction = await edgerouteTransactionPromise; + + expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value'); +}); + +test('Faulty edge routes', async ({ request }) => { + const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { + return ( + errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error' && + errorEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + request.get('/api/error-edge-endpoint').catch(() => { + // Noop + }); + + const [edgerouteTransaction, errorEvent] = await Promise.all([ + test.step('should create a transaction', () => edgerouteTransactionPromise), + test.step('should create an error event', () => errorEventPromise), + ]); + + test.step('should create transactions with the right fields', () => { + expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + }); + + test.step('should have scope isolation', () => { + expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + }); +}); + +test('Should not create spans for outgoing Sentry requests on edge routes', async ({ request }) => { + // Ensure no http.client transaction is created for any orphaned request + waitForTransaction('nextjs-app-dir', async transactionEvent => { + if (transactionEvent.contexts?.trace?.op === 'http.client') { + throw new Error(`Should not receive http.client transaction, but got: ${transactionEvent.transaction}`); + } + return false; + }); + + // We hit the endpoint three times and check that nowhere a http.client span for Sentry is to be found + // this way, we ensure that nothing is sent to Sentry in a follow up span + const edgerouteTransactionPromise1 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.get('/api/edge-endpoint', { + headers: { + 'x-yeet': 'test-value', + }, + }); + + const edgerouteTransactionPromise2 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.get('/api/edge-endpoint', { + headers: { + 'x-yeet': 'test-value-2', + }, + }); + + const edgerouteTransactionPromise3 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'GET /api/edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.get('/api/edge-endpoint', { + headers: { + 'x-yeet': 'test-value-3', + }, + }); + + const [edgerouteTransaction1, edgerouteTransaction2, edgerouteTransaction3] = await Promise.all([ + edgerouteTransactionPromise1, + edgerouteTransactionPromise2, + edgerouteTransactionPromise3, + ]); + + expect(edgerouteTransaction1.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction2.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction3.contexts?.trace?.op).toBe('http.server'); + + expect(edgerouteTransaction1.spans?.length).toBe(1); + expect(edgerouteTransaction2.spans?.length).toBe(1); + expect(edgerouteTransaction3.spans?.length).toBe(1); + + expect(edgerouteTransaction1.spans?.[0].description).toBe('handler (/api/edge-endpoint)'); + expect(edgerouteTransaction2.spans?.[0].description).toBe('handler (/api/edge-endpoint)'); + expect(edgerouteTransaction3.spans?.[0].description).toBe('handler (/api/edge-endpoint)'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 02f2b6dc4f24..1df9e662ad0b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -119,6 +119,63 @@ test.describe('Edge runtime', () => { expect(routehandlerError.transaction).toBe('DELETE /route-handlers/[param]/edge'); }); + + test('should not create spans for outgoing Sentry requests on edge routes', async ({ request }) => { + // Ensure no http.client transaction is created for any orphaned request + waitForTransaction('nextjs-app-dir', async transactionEvent => { + if (transactionEvent.contexts?.trace?.op === 'http.client') { + throw new Error(`Should not receive http.client transaction, but got: ${transactionEvent.transaction}`); + } + return false; + }); + + // We hit the endpoint three times and check that nowhere a http.client span for Sentry is to be found + // this way, we ensure that nothing is sent to Sentry in a follow up span + const edgerouteTransactionPromise1 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.patch('/route-handlers/bar/edge'); + + const edgerouteTransactionPromise2 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.patch('/route-handlers/bar/edge'); + + const edgerouteTransactionPromise3 = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); + }); + + await request.patch('/route-handlers/bar/edge'); + + const [edgerouteTransaction1, edgerouteTransaction2, edgerouteTransaction3] = await Promise.all([ + edgerouteTransactionPromise1, + edgerouteTransactionPromise2, + edgerouteTransactionPromise3, + ]); + + expect(edgerouteTransaction1.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction2.contexts?.trace?.op).toBe('http.server'); + expect(edgerouteTransaction3.contexts?.trace?.op).toBe('http.server'); + + expect(edgerouteTransaction1.spans?.length).toBe(1); + expect(edgerouteTransaction2.spans?.length).toBe(1); + expect(edgerouteTransaction3.spans?.length).toBe(1); + + expect(edgerouteTransaction1.spans?.[0].description).toBe('GET https://github.com'); + expect(edgerouteTransaction2.spans?.[0].description).toBe('GET https://github.com'); + expect(edgerouteTransaction3.spans?.[0].description).toBe('GET https://github.com'); + }); }); test('should not crash route handlers that are configured with `export const dynamic = "error"`', async ({ diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 2ee9f55c78a1..29c9e54be99c 100644 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -17,6 +17,7 @@ import * as diagch from 'diagnostics_channel'; import { NODE_MAJOR, NODE_MINOR } from '../../nodeVersion'; import { mergeBaggageHeaders } from '../../utils/baggage'; import type { UndiciRequest, UndiciResponse } from './types'; +import { isNextEdgeRuntime } from '../../utils/isNextEdgeRuntime'; const SENTRY_TRACE_HEADER = 'sentry-trace'; const SENTRY_BAGGAGE_HEADER = 'baggage'; @@ -238,7 +239,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase = {}): UndiciI const instrumentationConfig = { requireParentforSpans: false, ignoreRequestHook: request => { + // Never instrument outgoing requests in Edge Runtime + // This can be a problem when running in Next.js Edge Runtime in dev, + // as there edge is simulated but still uses Node under the hood, leaving to problems + if (isNextEdgeRuntime()) { + return true; + } + const url = getAbsoluteUrl(request.origin, request.path); const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); diff --git a/packages/node/src/utils/isNextEdgeRuntime.ts b/packages/node/src/utils/isNextEdgeRuntime.ts new file mode 100644 index 000000000000..14dbed335cf6 --- /dev/null +++ b/packages/node/src/utils/isNextEdgeRuntime.ts @@ -0,0 +1,8 @@ +/** + * Returns true if the current runtime is Next.js Edge Runtime. + * + * @returns `true` if the current runtime is Next.js Edge Runtime, `false` otherwise. + */ +export function isNextEdgeRuntime(): boolean { + return process.env.NEXT_RUNTIME === 'edge'; +} diff --git a/yarn.lock b/yarn.lock index ac3970cfb153..a44f353e348e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29253,7 +29253,7 @@ vite@^5.0.0, vite@^5.4.11, vite@^5.4.5: optionalDependencies: fsevents "~2.3.3" -vitefu@^0.2.2, vitefu@^0.2.4, vitefu@^0.2.5: +vitefu@^0.2.2, vitefu@^0.2.4: version "0.2.5" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q== From 3cab755c23408e2dc0c0f1af7a1f85f2f592807a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 11:19:16 +0200 Subject: [PATCH 2/7] fix by looking at url :( --- .../node-fetch/SentryNodeFetchInstrumentation.ts | 15 +++++++++------ .../node/src/integrations/node-fetch/index.ts | 16 ++++++++-------- packages/node/src/utils/isNextEdgeRuntime.ts | 8 -------- 3 files changed, 17 insertions(+), 22 deletions(-) delete mode 100644 packages/node/src/utils/isNextEdgeRuntime.ts diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 29c9e54be99c..75bd5c30d705 100644 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -2,7 +2,7 @@ import { context } from '@opentelemetry/api'; import { isTracingSuppressed, VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; -import type { SanitizedRequestData } from '@sentry/core'; +import { isSentryRequestUrl, SanitizedRequestData } from '@sentry/core'; import { addBreadcrumb, getBreadcrumbLogLevelFromHttpStatusCode, @@ -17,7 +17,6 @@ import * as diagch from 'diagnostics_channel'; import { NODE_MAJOR, NODE_MINOR } from '../../nodeVersion'; import { mergeBaggageHeaders } from '../../utils/baggage'; import type { UndiciRequest, UndiciResponse } from './types'; -import { isNextEdgeRuntime } from '../../utils/isNextEdgeRuntime'; const SENTRY_TRACE_HEADER = 'sentry-trace'; const SENTRY_BAGGAGE_HEADER = 'baggage'; @@ -239,10 +238,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase = {}): UndiciI const instrumentationConfig = { requireParentforSpans: false, ignoreRequestHook: request => { - // Never instrument outgoing requests in Edge Runtime - // This can be a problem when running in Next.js Edge Runtime in dev, - // as there edge is simulated but still uses Node under the hood, leaving to problems - if (isNextEdgeRuntime()) { - return true; - } - const url = getAbsoluteUrl(request.origin, request.path); const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); + // Normally, we should not need this, because `suppressTracing` should take care of this + // However, in Next.js Edge Runtime in dev, there is a bug where the edge is simulated but still uses Node under the hood, leading to problems + // So we make sure to ignore outgoing requests to Sentry endpoints + if (isSentryRequestUrl(url, getClient())) { + return true; + } + return !!shouldIgnore; }, startSpanHook: () => { diff --git a/packages/node/src/utils/isNextEdgeRuntime.ts b/packages/node/src/utils/isNextEdgeRuntime.ts deleted file mode 100644 index 14dbed335cf6..000000000000 --- a/packages/node/src/utils/isNextEdgeRuntime.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Returns true if the current runtime is Next.js Edge Runtime. - * - * @returns `true` if the current runtime is Next.js Edge Runtime, `false` otherwise. - */ -export function isNextEdgeRuntime(): boolean { - return process.env.NEXT_RUNTIME === 'edge'; -} From d80c681256e00400557159c64c787dbfd3a1a18d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 13:18:04 +0200 Subject: [PATCH 3/7] use event processor instead, bandaid! --- .../tests/route-handlers.test.ts | 6 +++--- packages/nextjs/src/server/index.ts | 18 ++++++++++++++---- .../SentryNodeFetchInstrumentation.ts | 17 +++++------------ .../node/src/integrations/node-fetch/index.ts | 10 +--------- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 1df9e662ad0b..9602b2118564 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -172,9 +172,9 @@ test.describe('Edge runtime', () => { expect(edgerouteTransaction2.spans?.length).toBe(1); expect(edgerouteTransaction3.spans?.length).toBe(1); - expect(edgerouteTransaction1.spans?.[0].description).toBe('GET https://github.com'); - expect(edgerouteTransaction2.spans?.[0].description).toBe('GET https://github.com'); - expect(edgerouteTransaction3.spans?.[0].description).toBe('GET https://github.com'); + expect(edgerouteTransaction1.spans?.[0].description).toBe('GET https://github.com/'); + expect(edgerouteTransaction2.spans?.[0].description).toBe('GET https://github.com/'); + expect(edgerouteTransaction3.spans?.[0].description).toBe('GET https://github.com/'); }); }); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index a6594e7fae1e..ac1937468b3e 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -6,7 +6,7 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; -import type { EventProcessor } from '@sentry/core'; +import { EventProcessor, isSentryRequestUrl } from '@sentry/core'; import { applySdkMetadata, extractTraceparentData, @@ -203,7 +203,7 @@ export function init(options: NodeOptions): NodeClient | undefined { } }); - getGlobalScope().addEventProcessor( + client?.addEventProcessor( Object.assign( (event => { if (event.type === 'transaction') { @@ -260,6 +260,16 @@ export function init(options: NodeOptions): NodeClient | undefined { } } + // On Edge Runtime, fetch requests can leak to the Node runtime fetch instrumentation + // and end up being sent as transactions to Sentry. + // We filter them here based on URL + if (event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'http.client') { + const url = event.contexts?.trace?.data?.['url']; + if (url && isSentryRequestUrl(url, client)) { + return null; + } + } + return event; } else { return event; @@ -269,7 +279,7 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - getGlobalScope().addEventProcessor( + client?.addEventProcessor( Object.assign( ((event, hint) => { if (event.type !== undefined) { @@ -364,7 +374,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }); if (process.env.NODE_ENV === 'development') { - getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); + client?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 75bd5c30d705..53228ef25fc9 100644 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -2,16 +2,16 @@ import { context } from '@opentelemetry/api'; import { isTracingSuppressed, VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; -import { isSentryRequestUrl, SanitizedRequestData } from '@sentry/core'; -import { - addBreadcrumb, +import type { +SanitizedRequestData , +} from '@sentry/core'; +import { addBreadcrumb, getBreadcrumbLogLevelFromHttpStatusCode, getClient, getSanitizedUrlString, getTraceData, LRUMap, - parseUrl, -} from '@sentry/core'; + parseUrl} from '@sentry/core'; import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry'; import * as diagch from 'diagnostics_channel'; import { NODE_MAJOR, NODE_MINOR } from '../../nodeVersion'; @@ -246,13 +246,6 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase = {}): UndiciI const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; const shouldIgnore = _ignoreOutgoingRequests && url && _ignoreOutgoingRequests(url); - // Normally, we should not need this, because `suppressTracing` should take care of this - // However, in Next.js Edge Runtime in dev, there is a bug where the edge is simulated but still uses Node under the hood, leading to problems - // So we make sure to ignore outgoing requests to Sentry endpoints - if (isSentryRequestUrl(url, getClient())) { - return true; - } - return !!shouldIgnore; }, startSpanHook: () => { From 5dc90f8534b8f358a18d549b8559ea226f6dc132 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 13:27:42 +0200 Subject: [PATCH 4/7] fix linting --- .../node-fetch/SentryNodeFetchInstrumentation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 53228ef25fc9..2ee9f55c78a1 100644 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -2,16 +2,16 @@ import { context } from '@opentelemetry/api'; import { isTracingSuppressed, VERSION } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; -import type { -SanitizedRequestData , -} from '@sentry/core'; -import { addBreadcrumb, +import type { SanitizedRequestData } from '@sentry/core'; +import { + addBreadcrumb, getBreadcrumbLogLevelFromHttpStatusCode, getClient, getSanitizedUrlString, getTraceData, LRUMap, - parseUrl} from '@sentry/core'; + parseUrl, +} from '@sentry/core'; import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry'; import * as diagch from 'diagnostics_channel'; import { NODE_MAJOR, NODE_MINOR } from '../../nodeVersion'; From 6efbca39cd257cdcd347c299f29f96ddaa8d0260 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 13:44:39 +0200 Subject: [PATCH 5/7] fix linting --- packages/nextjs/src/server/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index ac1937468b3e..f871fa2ee11d 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -6,7 +6,7 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; -import { EventProcessor, isSentryRequestUrl } from '@sentry/core'; +import type { EventProcessor } from '@sentry/core'; import { applySdkMetadata, extractTraceparentData, @@ -17,6 +17,7 @@ import { getIsolationScope, getRootSpan, GLOBAL_OBJ, + isSentryRequestUrl, logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, From 8373f6dd9bab766ea5c06c12a0eeb7500a4ae158 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 14:55:15 +0200 Subject: [PATCH 6/7] more reliable test --- .../tests/route-handlers.test.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 9602b2118564..485f7334e77e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -168,13 +168,25 @@ test.describe('Edge runtime', () => { expect(edgerouteTransaction2.contexts?.trace?.op).toBe('http.server'); expect(edgerouteTransaction3.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction1.spans?.length).toBe(1); - expect(edgerouteTransaction2.spans?.length).toBe(1); - expect(edgerouteTransaction3.spans?.length).toBe(1); - - expect(edgerouteTransaction1.spans?.[0].description).toBe('GET https://github.com/'); - expect(edgerouteTransaction2.spans?.[0].description).toBe('GET https://github.com/'); - expect(edgerouteTransaction3.spans?.[0].description).toBe('GET https://github.com/'); + expect(edgerouteTransaction1.spans).toContainEqual({ + description: 'GET https://github.com/', + }); + expect(edgerouteTransaction2.spans).toContainEqual({ + description: 'GET https://github.com/', + }); + expect(edgerouteTransaction3.spans).toContainEqual({ + description: 'GET https://github.com/', + }); + // Does not contain span that is sent to the event proxy server + expect(edgerouteTransaction1.spans).not.toContainEqual({ + description: expect.stringContaining('https://localhost:3031'), + }); + expect(edgerouteTransaction2.spans).not.toContainEqual({ + description: expect.stringContaining('https://localhost:3031'), + }); + expect(edgerouteTransaction3.spans).not.toContainEqual({ + description: expect.stringContaining('https://localhost:3031'), + }); }); }); From 188a8ffdf39d14dc52d0ec189e41fd9dace63179 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 30 Jun 2025 14:57:55 +0200 Subject: [PATCH 7/7] fix test --- .../tests/route-handlers.test.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 485f7334e77e..4d0de1d0d169 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -168,25 +168,37 @@ test.describe('Edge runtime', () => { expect(edgerouteTransaction2.contexts?.trace?.op).toBe('http.server'); expect(edgerouteTransaction3.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction1.spans).toContainEqual({ - description: 'GET https://github.com/', - }); - expect(edgerouteTransaction2.spans).toContainEqual({ - description: 'GET https://github.com/', - }); - expect(edgerouteTransaction3.spans).toContainEqual({ - description: 'GET https://github.com/', - }); + expect(edgerouteTransaction1.spans).toContainEqual( + expect.objectContaining({ + description: 'GET https://github.com/', + }), + ); + expect(edgerouteTransaction2.spans).toContainEqual( + expect.objectContaining({ + description: 'GET https://github.com/', + }), + ); + expect(edgerouteTransaction3.spans).toContainEqual( + expect.objectContaining({ + description: 'GET https://github.com/', + }), + ); // Does not contain span that is sent to the event proxy server - expect(edgerouteTransaction1.spans).not.toContainEqual({ - description: expect.stringContaining('https://localhost:3031'), - }); - expect(edgerouteTransaction2.spans).not.toContainEqual({ - description: expect.stringContaining('https://localhost:3031'), - }); - expect(edgerouteTransaction3.spans).not.toContainEqual({ - description: expect.stringContaining('https://localhost:3031'), - }); + expect(edgerouteTransaction1.spans).not.toContainEqual( + expect.objectContaining({ + description: expect.stringContaining('https://localhost:3031'), + }), + ); + expect(edgerouteTransaction2.spans).not.toContainEqual( + expect.objectContaining({ + description: expect.stringContaining('https://localhost:3031'), + }), + ); + expect(edgerouteTransaction3.spans).not.toContainEqual( + expect.objectContaining({ + description: expect.stringContaining('https://localhost:3031'), + }), + ); }); });