Skip to content

Commit 74f7994

Browse files
JPeer264claude
andcommitted
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 <noreply@anthropic.com>
1 parent 94224ee commit 74f7994

2 files changed

Lines changed: 83 additions & 72 deletions

File tree

‎packages/nextjs/src/server/handleOnSpanStart.ts‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,99 @@ import {
33
HTTP_METHOD,
44
HTTP_REQUEST_METHOD,
55
HTTP_ROUTE,
6+
HTTP_TARGET,
67
SENTRY_ORIGIN,
8+
URL_QUERY,
79
} from '@sentry/conventions/attributes';
8-
import type { Client, Span } from '@sentry/core';
10+
import type { Client, Options, Span } from '@sentry/core';
911
import {
1012
getIsolationScope,
1113
getRootSpan,
1214
hasSpanStreamingEnabled,
1315
HTTP_SPAN_NAME_FALLBACK,
1416
spanToJSON,
1517
} from '@sentry/core';
18+
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
1619
import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
20+
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
1721
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
22+
import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus';
1823
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
1924
import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter';
2025
import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan';
26+
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2127
import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils';
22-
import { maybeStartCronCheckIn } from './vercelCronsMonitoring';
23-
import { maybeEnrichQueueConsumerSpan, maybeEnrichQueueProducerSpan } from './vercelQueuesMonitoring';
28+
import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan';
29+
import { maybeCompleteCronCheckIn, maybeStartCronCheckIn } from './vercelCronsMonitoring';
30+
import {
31+
maybeCleanupQueueSpan,
32+
maybeEnrichQueueConsumerSpan,
33+
maybeEnrichQueueProducerSpan,
34+
} from './vercelQueuesMonitoring';
35+
36+
export const NEXTJS_SERVER_IGNORE_SPANS: NonNullable<Options['ignoreSpans']> = [
37+
// Static assets (matches `_next/static` anywhere in the name to handle custom basePath)
38+
/^GET (\/.*)?\/_next\/static\//,
39+
// Dev source-map fetch endpoints
40+
/\/__nextjs_original-stack-frame/,
41+
// Pages router /404
42+
/^\/404$/,
43+
// App router /404 and /_not-found segments (any HTTP method)
44+
/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/,
45+
// Root transactions named "NextServer.getRequestHandler" containing useless tracing
46+
/^NextServer\.getRequestHandler$/,
47+
// Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION
48+
// (set in `dropMiddlewareTunnelRequests` during `spanStart`)
49+
{ attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } },
50+
];
51+
52+
/**
53+
* Registers the hooks that name, enrich and filter the spans and events of the Next.js server on `client`.
54+
*/
55+
export function addNextjsServerSpanHooks(client: Client): void {
56+
client.on('beforeSampling', ({ spanAttributes }, samplingDecision) => {
57+
// There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in
58+
// middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing
59+
// because we didn't get the chance to do `suppressTracing`, since this happens outside of userland.
60+
// We need to drop these spans.
61+
if (
62+
// eslint-disable-next-line typescript/no-deprecated
63+
(typeof spanAttributes[HTTP_TARGET] === 'string' &&
64+
// eslint-disable-next-line typescript/no-deprecated
65+
spanAttributes[HTTP_TARGET].includes('sentry_key') &&
66+
// eslint-disable-next-line typescript/no-deprecated
67+
spanAttributes[HTTP_TARGET].includes('sentry_client')) ||
68+
(typeof spanAttributes[URL_QUERY] === 'string' &&
69+
spanAttributes[URL_QUERY].includes('sentry_key') &&
70+
spanAttributes[URL_QUERY].includes('sentry_client'))
71+
) {
72+
samplingDecision.decision = false;
73+
}
74+
});
75+
76+
client.on('spanStart', span => handleOnSpanStart(span, client));
77+
78+
// Normalize name/op/source/status on the request root span at span end, before it is serialized into
79+
// a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles
80+
// pick up the changes from one place, and the cron/queue hooks below see the finalized status.
81+
client.on('spanEnd', span => {
82+
if (span !== getRootSpan(span)) {
83+
return;
84+
}
85+
86+
const mutableRootSpan = createLiveRootSpanAdapter(span);
87+
enhanceHandleRequestRootSpan(mutableRootSpan);
88+
enhanceMiddlewareRootSpan(mutableRootSpan);
89+
backfillHttpServerStatus(span);
90+
});
91+
92+
client.on('spanEnd', maybeCompleteCronCheckIn);
93+
client.on('spanEnd', maybeCleanupQueueSpan);
94+
95+
client.on('preprocessEvent', event => {
96+
setUrlProcessingMetadata(event);
97+
});
98+
}
2499

25100
/**
26101
* Handles the on span start event for Next.js spans.

‎packages/nextjs/src/server/index.ts‎

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
// import/export got a false positive, and affects most of our index barrel files
22
// can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703
33
/* eslint-disable import/export */
4-
import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes';
54
import type { EventProcessor } from '@sentry/core';
65
import {
76
_INTERNAL_getActiveClient,
87
applySdkMetadata,
98
debug,
109
getGlobalScope,
11-
getRootSpan,
1210
getVercelEnv,
1311
GLOBAL_OBJ,
1412
} from '@sentry/core';
@@ -17,20 +15,12 @@ import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sent
1715
import { DEBUG_BUILD } from '../common/debug-build';
1816
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
1917
import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils';
20-
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
2118
import { isBuild } from '../common/utils/isBuild';
2219
import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
23-
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2420
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
25-
import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan';
26-
import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus';
27-
import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter';
28-
import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan';
29-
import { handleOnSpanStart } from './handleOnSpanStart';
21+
import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from './handleOnSpanStart';
3022
import { prepareSafeIdGeneratorContext } from './prepareSafeIdGeneratorContext';
3123
import { nextjsUseCacheIntegration } from './useCacheInstrumentation';
32-
import { maybeCompleteCronCheckIn } from './vercelCronsMonitoring';
33-
import { maybeCleanupQueueSpan } from './vercelQueuesMonitoring';
3424

3525
export * from '@sentry/node';
3626

@@ -159,22 +149,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
159149
...cloudflareConfig,
160150
};
161151

162-
const nextjsIgnoreSpans: NonNullable<NodeOptions['ignoreSpans']> = [
163-
// Static assets (matches `_next/static` anywhere in the name to handle custom basePath)
164-
/^GET (\/.*)?\/_next\/static\//,
165-
// Dev source-map fetch endpoints
166-
/\/__nextjs_original-stack-frame/,
167-
// Pages router /404
168-
/^\/404$/,
169-
// App router /404 and /_not-found segments (any HTTP method)
170-
/^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/,
171-
// Root transactions named "NextServer.getRequestHandler" containing useless tracing
172-
/^NextServer\.getRequestHandler$/,
173-
// Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION
174-
// (set in `dropMiddlewareTunnelRequests` during `spanStart`)
175-
{ attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } },
176-
];
177-
opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans];
152+
opts.ignoreSpans = [...(opts.ignoreSpans || []), ...NEXTJS_SERVER_IGNORE_SPANS];
178153

179154
if (DEBUG_BUILD && opts.debug) {
180155
debug.enable();
@@ -196,44 +171,9 @@ export function init(options: NodeOptions): NodeClient | undefined {
196171

197172
const client = isOwnedByCloudflare ? undefined : nodeInit(opts);
198173

199-
client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => {
200-
// There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in
201-
// middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing
202-
// because we didn't get the chance to do `suppressTracing`, since this happens outside of userland.
203-
// We need to drop these spans.
204-
if (
205-
// eslint-disable-next-line typescript/no-deprecated
206-
(typeof spanAttributes[HTTP_TARGET] === 'string' &&
207-
// eslint-disable-next-line typescript/no-deprecated
208-
spanAttributes[HTTP_TARGET].includes('sentry_key') &&
209-
// eslint-disable-next-line typescript/no-deprecated
210-
spanAttributes[HTTP_TARGET].includes('sentry_client')) ||
211-
(typeof spanAttributes[URL_QUERY] === 'string' &&
212-
spanAttributes[URL_QUERY].includes('sentry_key') &&
213-
spanAttributes[URL_QUERY].includes('sentry_client'))
214-
) {
215-
samplingDecision.decision = false;
216-
}
217-
});
218-
219-
client?.on('spanStart', span => handleOnSpanStart(span, client));
220-
221-
// Normalize name/op/source/status on the request root span at span end, before it is serialized into
222-
// a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles
223-
// pick up the changes from one place, and the cron/queue hooks below see the finalized status.
224-
client?.on('spanEnd', span => {
225-
if (span !== getRootSpan(span)) {
226-
return;
227-
}
228-
229-
const mutableRootSpan = createLiveRootSpanAdapter(span);
230-
enhanceHandleRequestRootSpan(mutableRootSpan);
231-
enhanceMiddlewareRootSpan(mutableRootSpan);
232-
backfillHttpServerStatus(span);
233-
});
234-
235-
client?.on('spanEnd', maybeCompleteCronCheckIn);
236-
client?.on('spanEnd', maybeCleanupQueueSpan);
174+
if (client) {
175+
addNextjsServerSpanHooks(client);
176+
}
237177

238178
// On the client, not the global scope, so a later `init()` after
239179
// `close()` does not stack another copy. In a request of `withSentry` on
@@ -282,10 +222,6 @@ export function init(options: NodeOptions): NodeClient | undefined {
282222
),
283223
);
284224

285-
client?.on('preprocessEvent', event => {
286-
setUrlProcessingMetadata(event);
287-
});
288-
289225
if (process.env.NODE_ENV === 'development') {
290226
eventProcessorTarget?.addEventProcessor(devErrorSymbolicationEventProcessor);
291227
}

0 commit comments

Comments
 (0)