Skip to content

Commit f09e415

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 aab82c2 commit f09e415

2 files changed

Lines changed: 84 additions & 80 deletions

File tree

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
HTTP_METHOD,
44
HTTP_REQUEST_METHOD,
55
HTTP_ROUTE,
6+
HTTP_TARGET,
7+
URL_QUERY,
68
} from '@sentry/conventions/attributes';
7-
import type { Client, Span } from '@sentry/core';
9+
import type { Client, Options, Span } from '@sentry/core';
810
import {
911
getIsolationScope,
1012
getRootSpan,
@@ -13,14 +15,87 @@ import {
1315
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
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: 6 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
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';
6-
import {
7-
applySdkMetadata,
8-
debug,
9-
getClient,
10-
getGlobalScope,
11-
getRootSpan,
12-
getVercelEnv,
13-
GLOBAL_OBJ,
14-
} from '@sentry/core';
5+
import { applySdkMetadata, debug, getClient, getGlobalScope, getVercelEnv, GLOBAL_OBJ } from '@sentry/core';
156
import type { NodeClient, NodeOptions } from '@sentry/node';
167
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
178
import { DEBUG_BUILD } from '../common/debug-build';
189
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
1910
import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils';
20-
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
2111
import { isBuild } from '../common/utils/isBuild';
2212
import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd';
23-
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
2413
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';
14+
import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from './handleOnSpanStart';
3015
import { prepareSafeIdGeneratorContext } from './prepareSafeIdGeneratorContext';
3116
import { nextjsUseCacheIntegration } from './useCacheInstrumentation';
32-
import { maybeCompleteCronCheckIn } from './vercelCronsMonitoring';
33-
import { maybeCleanupQueueSpan } from './vercelQueuesMonitoring';
3417

3518
export * from '@sentry/node';
3619

@@ -159,22 +142,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
159142
...cloudflareConfig,
160143
};
161144

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];
145+
opts.ignoreSpans = [...(opts.ignoreSpans || []), ...NEXTJS_SERVER_IGNORE_SPANS];
178146

179147
if (DEBUG_BUILD && opts.debug) {
180148
debug.enable();
@@ -195,44 +163,9 @@ export function init(options: NodeOptions): NodeClient | undefined {
195163

196164
const client = isOwnedByCloudflare ? undefined : nodeInit(opts);
197165

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

237170
getGlobalScope().addEventProcessor(
238171
Object.assign(
@@ -276,10 +209,6 @@ export function init(options: NodeOptions): NodeClient | undefined {
276209
),
277210
);
278211

279-
client?.on('preprocessEvent', event => {
280-
setUrlProcessingMetadata(event);
281-
});
282-
283212
if (process.env.NODE_ENV === 'development') {
284213
getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor);
285214
}

0 commit comments

Comments
 (0)