-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(nextjs): Extract tracing logic from server component wrapper templates #18408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
45f14ab
fbb8f37
2a2613d
960dcd1
40f32cc
805e1af
076c4df
6b15148
c6751b3
8f07b99
e30ea5c
4fec975
ecf4dab
1b8b439
01cbe44
ba33529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| export const ATTR_NEXT_SPAN_TYPE = 'next.span_type'; | ||
| export const ATTR_NEXT_SPAN_NAME = 'next.span_name'; | ||
| export const ATTR_NEXT_ROUTE = 'next.route'; | ||
| export const ATTR_NEXT_SPAN_DESCRIPTION = 'next.span_description'; | ||
| export const ATTR_NEXT_SEGMENT = 'next.segment'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,9 @@ | ||
| import type { RequestEventData } from '@sentry/core'; | ||
| import { | ||
| captureException, | ||
| getActiveSpan, | ||
| getCapturedScopesOnSpan, | ||
| getRootSpan, | ||
| handleCallbackErrors, | ||
| propagationContextFromHeaders, | ||
| Scope, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, | ||
| setCapturedScopesOnSpan, | ||
| SPAN_STATUS_ERROR, | ||
| SPAN_STATUS_OK, | ||
| startSpanManual, | ||
| winterCGHeadersToDict, | ||
| withIsolationScope, | ||
| withScope, | ||
| } from '@sentry/core'; | ||
| import { captureException, handleCallbackErrors, winterCGHeadersToDict, withIsolationScope } from '@sentry/core'; | ||
| import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; | ||
| import type { ServerComponentContext } from '../common/types'; | ||
| import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; | ||
| import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; | ||
| import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; | ||
| import { commonObjectToIsolationScope } from './utils/tracingUtils'; | ||
|
|
||
| /** | ||
| * Wraps an `app` directory server component with Sentry error instrumentation. | ||
|
Comment on lines
6
to
16
This comment was marked as outdated.
Sorry, something went wrong. |
||
|
|
@@ -31,22 +13,13 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any> | |
| appDirComponent: F, | ||
| context: ServerComponentContext, | ||
| ): F { | ||
| const { componentRoute, componentType } = context; | ||
| // Even though users may define server components as async functions, for the client bundles | ||
| // Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use` | ||
| // hook. 🤯 | ||
| return new Proxy(appDirComponent, { | ||
| apply: (originalFunction, thisArg, args) => { | ||
| const requestTraceId = getActiveSpan()?.spanContext().traceId; | ||
| const isolationScope = commonObjectToIsolationScope(context.headers); | ||
|
|
||
| const activeSpan = getActiveSpan(); | ||
| if (activeSpan) { | ||
| const rootSpan = getRootSpan(activeSpan); | ||
| const { scope } = getCapturedScopesOnSpan(rootSpan); | ||
| setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); | ||
| } | ||
|
|
||
| const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; | ||
|
Comment on lines
+28
to
30
This comment was marked as outdated.
Sorry, something went wrong. |
||
|
|
||
| isolationScope.setSDKProcessingMetadata({ | ||
|
|
@@ -56,72 +29,26 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any> | |
| }); | ||
|
|
||
| return withIsolationScope(isolationScope, () => { | ||
| return withScope(scope => { | ||
| scope.setTransactionName(`${componentType} Server Component (${componentRoute})`); | ||
|
|
||
| if (process.env.NEXT_RUNTIME === 'edge') { | ||
| const propagationContext = commonObjectToPropagationContext( | ||
| context.headers, | ||
| propagationContextFromHeaders(headersDict?.['sentry-trace'], headersDict?.['baggage']), | ||
| ); | ||
|
|
||
| if (requestTraceId) { | ||
| propagationContext.traceId = requestTraceId; | ||
| } | ||
|
|
||
| scope.setPropagationContext(propagationContext); | ||
| } | ||
|
|
||
| const activeSpan = getActiveSpan(); | ||
| if (activeSpan) { | ||
| const rootSpan = getRootSpan(activeSpan); | ||
| const sentryTrace = headersDict?.['sentry-trace']; | ||
| if (sentryTrace) { | ||
| rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); | ||
| } | ||
| } | ||
|
|
||
| return startSpanManual( | ||
| { | ||
| op: 'function.nextjs', | ||
| name: `${componentType} Server Component (${componentRoute})`, | ||
| attributes: { | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', | ||
| [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.server_component', | ||
| 'sentry.nextjs.ssr.function.type': componentType, | ||
| 'sentry.nextjs.ssr.function.route': componentRoute, | ||
| }, | ||
| }, | ||
| span => { | ||
| return handleCallbackErrors( | ||
| () => originalFunction.apply(thisArg, args), | ||
| error => { | ||
| // When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?" | ||
| // The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response. | ||
| if (isNotFoundNavigationError(error)) { | ||
| // We don't want to report "not-found"s | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); | ||
| } else if (isRedirectNavigationError(error)) { | ||
| // We don't want to report redirects | ||
| span.setStatus({ code: SPAN_STATUS_OK }); | ||
| } else { | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); | ||
| captureException(error, { | ||
| mechanism: { | ||
| handled: false, | ||
| type: 'auto.function.nextjs.server_component', | ||
| }, | ||
| }); | ||
| } | ||
| return handleCallbackErrors( | ||
| () => originalFunction.apply(thisArg, args), | ||
| error => { | ||
| if (isNotFoundNavigationError(error)) { | ||
| // We don't want to report "not-found"s | ||
| } else if (isRedirectNavigationError(error)) { | ||
| // We don't want to report redirects | ||
| } else { | ||
| captureException(error, { | ||
| mechanism: { | ||
| handled: false, | ||
| type: 'auto.function.nextjs.server_component', | ||
| }, | ||
| () => { | ||
| span.end(); | ||
| waitUntil(flushSafelyWithTimeout()); | ||
| }, | ||
| ); | ||
| }, | ||
| ); | ||
| }); | ||
| }); | ||
| } | ||
|
Comment on lines
+47
to
57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The refactored 🔍 Detailed AnalysisIn the refactored 💡 Suggested FixEnsure the active span is explicitly ended after the wrapped function executes. A potential fix is to get the active span before the function runs and call 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we delegate the entire span lifecycle to next |
||
| }, | ||
| () => { | ||
| waitUntil(flushSafelyWithTimeout()); | ||
| }, | ||
| ); | ||
| }); | ||
| }, | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { context } from '@opentelemetry/api'; | ||
| import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_METHOD } from '@opentelemetry/semantic-conventions'; | ||
| import type { Span } from '@sentry/core'; | ||
| import { | ||
| getCapturedScopesOnSpan, | ||
| getCurrentScope, | ||
| getIsolationScope, | ||
| getRootSpan, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, | ||
| setCapturedScopesOnSpan, | ||
| spanToJSON, | ||
| } from '@sentry/core'; | ||
| import { getScopesFromContext } from '@sentry/opentelemetry'; | ||
| import { | ||
| ATTR_NEXT_ROUTE, | ||
| ATTR_NEXT_SEGMENT, | ||
| ATTR_NEXT_SPAN_NAME, | ||
| ATTR_NEXT_SPAN_TYPE, | ||
| } from '../common/nextSpanAttributes'; | ||
| import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; | ||
| import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; | ||
| import { getEnhancedResolveSegmentSpanName, isResolveSegmentSpan } from '../common/utils/tracingUtils'; | ||
|
|
||
| /** | ||
| * Handles the on span start event for Next.js spans. | ||
| * This function is used to enhance the span with additional information such as the route, the method, the headers, etc. | ||
| * It is called for every span that is started by Next.js. | ||
| * @param span The span that is starting. | ||
| */ | ||
| export function handleOnSpanStart(span: Span): void { | ||
| const spanAttributes = spanToJSON(span).data; | ||
| const rootSpan = getRootSpan(span); | ||
| const isRootSpan = span === rootSpan; | ||
|
|
||
| dropMiddlewareTunnelRequests(span, spanAttributes); | ||
|
|
||
| // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted | ||
| // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. | ||
| if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') { | ||
| const rootSpanAttributes = spanToJSON(rootSpan).data; | ||
| // Only hoist the http.route attribute if the transaction doesn't already have it | ||
| if ( | ||
| // eslint-disable-next-line deprecation/deprecation | ||
| (rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) && | ||
| !rootSpanAttributes?.[ATTR_HTTP_ROUTE] | ||
| ) { | ||
| const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, ''); | ||
| rootSpan.updateName(route); | ||
| rootSpan.setAttribute(ATTR_HTTP_ROUTE, route); | ||
| // Preserving the original attribute despite internally not depending on it | ||
| rootSpan.setAttribute(ATTR_NEXT_ROUTE, route); | ||
| } | ||
| } | ||
|
|
||
| if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { | ||
| const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME]; | ||
| if (typeof middlewareName === 'string') { | ||
| rootSpan.updateName(middlewareName); | ||
| rootSpan.setAttribute(ATTR_HTTP_ROUTE, middlewareName); | ||
| rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName); | ||
| } | ||
| span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); | ||
| } | ||
|
|
||
| // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans | ||
| // with patterns (e.g. http.server spans) that will produce confusing data. | ||
| if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { | ||
| span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); | ||
| } | ||
|
|
||
| if (isRootSpan) { | ||
| const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers; | ||
| addHeadersAsAttributes(headers, rootSpan); | ||
| } | ||
|
|
||
| // We want to fork the isolation scope for incoming requests | ||
| if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) { | ||
| const scopes = getCapturedScopesOnSpan(span); | ||
|
|
||
| const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); | ||
| const scope = scopes.scope || getCurrentScope(); | ||
|
|
||
| const currentScopesPointer = getScopesFromContext(context.active()); | ||
| if (currentScopesPointer) { | ||
| currentScopesPointer.isolationScope = isolationScope; | ||
| } | ||
|
|
||
| setCapturedScopesOnSpan(span, scope, isolationScope); | ||
| } | ||
|
|
||
| // Enhancing server component span names | ||
| if (isResolveSegmentSpan(spanAttributes)) { | ||
| // type conversion is safe because we already know the attribute is a string | ||
| span.updateName(getEnhancedResolveSegmentSpanName(spanAttributes[ATTR_NEXT_SEGMENT] as string)); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.