Skip to content

Commit 45f14ab

Browse files
committed
handle segment spans
1 parent 98854d4 commit 45f14ab

File tree

5 files changed

+153
-166
lines changed

5 files changed

+153
-166
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const ATTR_NEXT_SPAN_TYPE = 'next.span_type';
22
export const ATTR_NEXT_SPAN_NAME = 'next.span_name';
33
export const ATTR_NEXT_ROUTE = 'next.route';
4+
export const ATTR_NEXT_SPAN_DESCRIPTION = 'next.span_description';
5+
export const ATTR_NEXT_SEGMENT = 'next.segment';

packages/nextjs/src/common/utils/tracingUtils.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { PropagationContext } from '@sentry/core';
1+
import type { PropagationContext, SpanAttributes } from '@sentry/core';
22
import { debug, getActiveSpan, getRootSpan, GLOBAL_OBJ, Scope, spanToJSON, startNewTrace } from '@sentry/core';
33
import { DEBUG_BUILD } from '../debug-build';
4+
import { ATTR_NEXT_SEGMENT, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
45
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
56

67
const commonPropagationContextMap = new WeakMap<object, PropagationContext>();
@@ -108,3 +109,33 @@ export function dropNextjsRootContext(): void {
108109
}
109110
}
110111
}
112+
113+
/**
114+
* Checks if the span is a resolve segment span.
115+
* @param spanAttributes The attributes of the span to check.
116+
* @returns True if the span is a resolve segment span, false otherwise.
117+
*/
118+
export function isResolveSegmentSpan(spanAttributes: SpanAttributes): boolean {
119+
return (
120+
spanAttributes[ATTR_NEXT_SPAN_TYPE] === 'NextNodeServer.getLayoutOrPageModule' &&
121+
spanAttributes[ATTR_NEXT_SPAN_NAME] === 'resolve segment modules' &&
122+
typeof spanAttributes[ATTR_NEXT_SEGMENT] === 'string'
123+
);
124+
}
125+
126+
/**
127+
* Returns the enhanced name for a resolve segment span.
128+
* @param segment The segment of the resolve segment span.
129+
* @returns The enhanced name for the resolve segment span.
130+
*/
131+
export function getEnhancedResolveSegmentSpanName(segment: string): string {
132+
if (segment === '__PAGE__') {
133+
return 'resolve page module';
134+
}
135+
136+
if (segment === '') {
137+
return 'resolve root layout module';
138+
}
139+
140+
return `resolve layout module "${segment}"`;
141+
}

packages/nextjs/src/common/wrapServerComponentWithSentry.ts

Lines changed: 21 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,9 @@
11
import type { RequestEventData } from '@sentry/core';
2-
import {
3-
captureException,
4-
getActiveSpan,
5-
getCapturedScopesOnSpan,
6-
getRootSpan,
7-
handleCallbackErrors,
8-
propagationContextFromHeaders,
9-
Scope,
10-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
11-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
12-
setCapturedScopesOnSpan,
13-
SPAN_STATUS_ERROR,
14-
SPAN_STATUS_OK,
15-
startSpanManual,
16-
winterCGHeadersToDict,
17-
withIsolationScope,
18-
withScope,
19-
} from '@sentry/core';
2+
import { captureException, handleCallbackErrors, winterCGHeadersToDict, withIsolationScope } from '@sentry/core';
203
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
214
import type { ServerComponentContext } from '../common/types';
225
import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd';
23-
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
24-
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
6+
import { commonObjectToIsolationScope } from './utils/tracingUtils';
257

268
/**
279
* Wraps an `app` directory server component with Sentry error instrumentation.
@@ -31,22 +13,13 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
3113
appDirComponent: F,
3214
context: ServerComponentContext,
3315
): F {
34-
const { componentRoute, componentType } = context;
3516
// Even though users may define server components as async functions, for the client bundles
3617
// Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use`
3718
// hook. 🤯
3819
return new Proxy(appDirComponent, {
3920
apply: (originalFunction, thisArg, args) => {
40-
const requestTraceId = getActiveSpan()?.spanContext().traceId;
4121
const isolationScope = commonObjectToIsolationScope(context.headers);
4222

43-
const activeSpan = getActiveSpan();
44-
if (activeSpan) {
45-
const rootSpan = getRootSpan(activeSpan);
46-
const { scope } = getCapturedScopesOnSpan(rootSpan);
47-
setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope);
48-
}
49-
5023
const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;
5124

5225
isolationScope.setSDKProcessingMetadata({
@@ -56,72 +29,26 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
5629
});
5730

5831
return withIsolationScope(isolationScope, () => {
59-
return withScope(scope => {
60-
scope.setTransactionName(`${componentType} Server Component (${componentRoute})`);
61-
62-
if (process.env.NEXT_RUNTIME === 'edge') {
63-
const propagationContext = commonObjectToPropagationContext(
64-
context.headers,
65-
propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']),
66-
);
67-
68-
if (requestTraceId) {
69-
propagationContext.traceId = requestTraceId;
70-
}
71-
72-
scope.setPropagationContext(propagationContext);
73-
}
74-
75-
const activeSpan = getActiveSpan();
76-
if (activeSpan) {
77-
const rootSpan = getRootSpan(activeSpan);
78-
const sentryTrace = headersDict?.['sentry-trace'];
79-
if (sentryTrace) {
80-
rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace);
81-
}
82-
}
83-
84-
return startSpanManual(
85-
{
86-
op: 'function.nextjs',
87-
name: `${componentType} Server Component (${componentRoute})`,
88-
attributes: {
89-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
90-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.server_component',
91-
'sentry.nextjs.ssr.function.type': componentType,
92-
'sentry.nextjs.ssr.function.route': componentRoute,
93-
},
94-
},
95-
span => {
96-
return handleCallbackErrors(
97-
() => originalFunction.apply(thisArg, args),
98-
error => {
99-
// When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?"
100-
// The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response.
101-
if (isNotFoundNavigationError(error)) {
102-
// We don't want to report "not-found"s
103-
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
104-
} else if (isRedirectNavigationError(error)) {
105-
// We don't want to report redirects
106-
span.setStatus({ code: SPAN_STATUS_OK });
107-
} else {
108-
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
109-
captureException(error, {
110-
mechanism: {
111-
handled: false,
112-
type: 'auto.function.nextjs.server_component',
113-
},
114-
});
115-
}
32+
return handleCallbackErrors(
33+
() => originalFunction.apply(thisArg, args),
34+
error => {
35+
if (isNotFoundNavigationError(error)) {
36+
// We don't want to report "not-found"s
37+
} else if (isRedirectNavigationError(error)) {
38+
// We don't want to report redirects
39+
} else {
40+
captureException(error, {
41+
mechanism: {
42+
handled: false,
43+
type: 'auto.function.nextjs.server_component',
11644
},
117-
() => {
118-
span.end();
119-
waitUntil(flushSafelyWithTimeout());
120-
},
121-
);
122-
},
123-
);
124-
});
45+
});
46+
}
47+
},
48+
() => {
49+
waitUntil(flushSafelyWithTimeout());
50+
},
51+
);
12552
});
12653
},
12754
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { context } from '@opentelemetry/api';
2+
import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_METHOD } from '@opentelemetry/semantic-conventions';
3+
import type { Span } from '@sentry/core';
4+
import {
5+
getCapturedScopesOnSpan,
6+
getCurrentScope,
7+
getIsolationScope,
8+
getRootSpan,
9+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
10+
setCapturedScopesOnSpan,
11+
spanToJSON,
12+
} from '@sentry/core';
13+
import { getScopesFromContext } from '@sentry/opentelemetry';
14+
import {
15+
ATTR_NEXT_ROUTE,
16+
ATTR_NEXT_SEGMENT,
17+
ATTR_NEXT_SPAN_NAME,
18+
ATTR_NEXT_SPAN_TYPE,
19+
} from '../common/nextSpanAttributes';
20+
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
21+
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
22+
import { getEnhancedResolveSegmentSpanName, isResolveSegmentSpan } from '../common/utils/tracingUtils';
23+
24+
/**
25+
* Handles the on span start event for Next.js spans.
26+
* This function is used to enhance the span with additional information such as the route, the method, the headers, etc.
27+
* It is called for every span that is started by Next.js.
28+
* @param span The span that is starting.
29+
*/
30+
export function handleOnSpanStart(span: Span): void {
31+
const spanAttributes = spanToJSON(span).data;
32+
const rootSpan = getRootSpan(span);
33+
const isRootSpan = span === rootSpan;
34+
35+
dropMiddlewareTunnelRequests(span, spanAttributes);
36+
37+
// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
38+
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
39+
if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') {
40+
const rootSpanAttributes = spanToJSON(rootSpan).data;
41+
// Only hoist the http.route attribute if the transaction doesn't already have it
42+
if (
43+
// eslint-disable-next-line deprecation/deprecation
44+
(rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) &&
45+
!rootSpanAttributes?.[ATTR_HTTP_ROUTE]
46+
) {
47+
const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, '');
48+
rootSpan.updateName(route);
49+
rootSpan.setAttribute(ATTR_HTTP_ROUTE, route);
50+
// Preserving the original attribute despite internally not depending on it
51+
rootSpan.setAttribute(ATTR_NEXT_ROUTE, route);
52+
}
53+
}
54+
55+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') {
56+
const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME];
57+
if (typeof middlewareName === 'string') {
58+
rootSpan.updateName(middlewareName);
59+
rootSpan.setAttribute(ATTR_HTTP_ROUTE, middlewareName);
60+
rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName);
61+
}
62+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
63+
}
64+
65+
// We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans
66+
// with patterns (e.g. http.server spans) that will produce confusing data.
67+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) {
68+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
69+
}
70+
71+
if (isRootSpan) {
72+
const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers;
73+
addHeadersAsAttributes(headers, rootSpan);
74+
}
75+
76+
// We want to fork the isolation scope for incoming requests
77+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) {
78+
const scopes = getCapturedScopesOnSpan(span);
79+
80+
const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
81+
const scope = scopes.scope || getCurrentScope();
82+
83+
const currentScopesPointer = getScopesFromContext(context.active());
84+
if (currentScopesPointer) {
85+
currentScopesPointer.isolationScope = isolationScope;
86+
}
87+
88+
setCapturedScopesOnSpan(span, scope, isolationScope);
89+
}
90+
91+
// Enhancing server component span names
92+
if (isResolveSegmentSpan(spanAttributes)) {
93+
// type conversion is safe because we already know the attribute is a string
94+
span.updateName(getEnhancedResolveSegmentSpanName(spanAttributes[ATTR_NEXT_SEGMENT] as string));
95+
}
96+
}

0 commit comments

Comments
 (0)