From 051bd518f9f6f14755091abcc33890be372a653d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 29 Sep 2026 15:54:47 +0200 Subject: [PATCH] ref(nextjs): Move the server span hooks into `handleOnSpanStart` The hooks that the server `init` registers on its client and its `ignoreSpans` list move into `addNextjsServerSpanHooks` and `NEXTJS_SERVER_IGNORE_SPANS` next to `handleOnSpanStart`. Code that does not import `@sentry/node` can then register them on another client. There is no behavior change. Co-Authored-By: Claude Opus 5.5 --- .../nextjs/src/server/handleOnSpanStart.ts | 81 +++++++++++++++++- packages/nextjs/src/server/index.ts | 83 ++----------------- 2 files changed, 84 insertions(+), 80 deletions(-) diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts index 5a7f3ea59772..89277c42b53c 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -3,8 +3,10 @@ import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, + HTTP_TARGET, + URL_QUERY, } from '@sentry/conventions/attributes'; -import type { Client, Span } from '@sentry/core'; +import type { Client, Options, Span } from '@sentry/core'; import { getIsolationScope, getRootSpan, @@ -13,14 +15,87 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; +import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan'; +import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils'; -import { maybeStartCronCheckIn } from './vercelCronsMonitoring'; -import { maybeEnrichQueueConsumerSpan, maybeEnrichQueueProducerSpan } from './vercelQueuesMonitoring'; +import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; +import { maybeCompleteCronCheckIn, maybeStartCronCheckIn } from './vercelCronsMonitoring'; +import { + maybeCleanupQueueSpan, + maybeEnrichQueueConsumerSpan, + maybeEnrichQueueProducerSpan, +} from './vercelQueuesMonitoring'; + +export const NEXTJS_SERVER_IGNORE_SPANS: NonNullable = [ + // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) + /^GET (\/.*)?\/_next\/static\//, + // Dev source-map fetch endpoints + /\/__nextjs_original-stack-frame/, + // Pages router /404 + /^\/404$/, + // App router /404 and /_not-found segments (any HTTP method) + /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, + // Root transactions named "NextServer.getRequestHandler" containing useless tracing + /^NextServer\.getRequestHandler$/, + // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION + // (set in `dropMiddlewareTunnelRequests` during `spanStart`) + { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, +]; + +/** + * Registers the hooks that name, enrich and filter the spans and events of the Next.js server on `client`. + */ +export function addNextjsServerSpanHooks(client: Client): void { + client.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { + // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in + // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing + // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. + // We need to drop these spans. + if ( + // eslint-disable-next-line typescript/no-deprecated + (typeof spanAttributes[HTTP_TARGET] === 'string' && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_key') && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_client')) || + (typeof spanAttributes[URL_QUERY] === 'string' && + spanAttributes[URL_QUERY].includes('sentry_key') && + spanAttributes[URL_QUERY].includes('sentry_client')) + ) { + samplingDecision.decision = false; + } + }); + + client.on('spanStart', span => handleOnSpanStart(span, client)); + + // Normalize name/op/source/status on the request root span at span end, before it is serialized into + // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles + // pick up the changes from one place, and the cron/queue hooks below see the finalized status. + client.on('spanEnd', span => { + if (span !== getRootSpan(span)) { + return; + } + + const mutableRootSpan = createLiveRootSpanAdapter(span); + enhanceHandleRequestRootSpan(mutableRootSpan); + enhanceMiddlewareRootSpan(mutableRootSpan); + backfillHttpServerStatus(span); + }); + + client.on('spanEnd', maybeCompleteCronCheckIn); + client.on('spanEnd', maybeCleanupQueueSpan); + + client.on('preprocessEvent', event => { + setUrlProcessingMetadata(event); + }); +} /** * Handles the on span start event for Next.js spans. diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 1e773fb36a10..884d046e30f2 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,36 +1,19 @@ // import/export got a false positive, and affects most of our index barrel files // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ -import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; import type { EventProcessor } from '@sentry/core'; -import { - applySdkMetadata, - debug, - getClient, - getGlobalScope, - getRootSpan, - getVercelEnv, - GLOBAL_OBJ, -} from '@sentry/core'; +import { applySdkMetadata, debug, getClient, getGlobalScope, getVercelEnv, GLOBAL_OBJ } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils'; -import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; -import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; -import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; -import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; -import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; -import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; -import { handleOnSpanStart } from './handleOnSpanStart'; +import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from './handleOnSpanStart'; import { prepareSafeIdGeneratorContext } from './prepareSafeIdGeneratorContext'; import { nextjsUseCacheIntegration } from './useCacheInstrumentation'; -import { maybeCompleteCronCheckIn } from './vercelCronsMonitoring'; -import { maybeCleanupQueueSpan } from './vercelQueuesMonitoring'; export * from '@sentry/node'; @@ -159,22 +142,7 @@ export function init(options: NodeOptions): NodeClient | undefined { ...cloudflareConfig, }; - const nextjsIgnoreSpans: NonNullable = [ - // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) - /^GET (\/.*)?\/_next\/static\//, - // Dev source-map fetch endpoints - /\/__nextjs_original-stack-frame/, - // Pages router /404 - /^\/404$/, - // App router /404 and /_not-found segments (any HTTP method) - /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, - // Root transactions named "NextServer.getRequestHandler" containing useless tracing - /^NextServer\.getRequestHandler$/, - // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION - // (set in `dropMiddlewareTunnelRequests` during `spanStart`) - { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, - ]; - opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans]; + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...NEXTJS_SERVER_IGNORE_SPANS]; if (DEBUG_BUILD && opts.debug) { debug.enable(); @@ -195,44 +163,9 @@ export function init(options: NodeOptions): NodeClient | undefined { const client = isOwnedByCloudflare ? undefined : nodeInit(opts); - client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { - // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in - // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing - // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. - // We need to drop these spans. - if ( - // eslint-disable-next-line typescript/no-deprecated - (typeof spanAttributes[HTTP_TARGET] === 'string' && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_key') && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_client')) || - (typeof spanAttributes[URL_QUERY] === 'string' && - spanAttributes[URL_QUERY].includes('sentry_key') && - spanAttributes[URL_QUERY].includes('sentry_client')) - ) { - samplingDecision.decision = false; - } - }); - - client?.on('spanStart', span => handleOnSpanStart(span, client)); - - // Normalize name/op/source/status on the request root span at span end, before it is serialized into - // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles - // pick up the changes from one place, and the cron/queue hooks below see the finalized status. - client?.on('spanEnd', span => { - if (span !== getRootSpan(span)) { - return; - } - - const mutableRootSpan = createLiveRootSpanAdapter(span); - enhanceHandleRequestRootSpan(mutableRootSpan); - enhanceMiddlewareRootSpan(mutableRootSpan); - backfillHttpServerStatus(span); - }); - - client?.on('spanEnd', maybeCompleteCronCheckIn); - client?.on('spanEnd', maybeCleanupQueueSpan); + if (client) { + addNextjsServerSpanHooks(client); + } getGlobalScope().addEventProcessor( Object.assign( @@ -276,10 +209,6 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - client?.on('preprocessEvent', event => { - setUrlProcessingMetadata(event); - }); - if (process.env.NODE_ENV === 'development') { getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); }