-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: develop
Are you sure you want to change the base?
Changes from all commits
45f14ab
fbb8f37
2a2613d
960dcd1
40f32cc
805e1af
076c4df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { PropsWithChildren } from 'react'; | ||
|
|
||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
| export default function Layout({ children }: PropsWithChildren<{}>) { | ||
| return ( | ||
| <div> | ||
| <p>DynamicLayout</p> | ||
| {children} | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export const dynamic = 'force-dynamic'; | ||
|
|
||
| export default async function Page() { | ||
| return ( | ||
| <div> | ||
| <p>Dynamic Page</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export async function generateMetadata() { | ||
| return { | ||
| title: 'I am dynamic page generated metadata', | ||
| }; | ||
| } |
| 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 |
|---|---|---|
|
|
@@ -2,26 +2,16 @@ import type { RequestEventData } from '@sentry/core'; | |
| import { | ||
| captureException, | ||
| getActiveSpan, | ||
| getCapturedScopesOnSpan, | ||
| getRootSpan, | ||
| getIsolationScope, | ||
| 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 { 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. | ||
|
|
@@ -31,22 +21,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; | ||
|
|
||
| isolationScope.setSDKProcessingMetadata({ | ||
|
|
@@ -55,74 +36,42 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any> | |
| } satisfies RequestEventData, | ||
| }); | ||
|
|
||
| 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); | ||
| } | ||
| return handleCallbackErrors( | ||
| () => originalFunction.apply(thisArg, args), | ||
| error => { | ||
| const isolationScope = getIsolationScope(); | ||
| const span = getActiveSpan(); | ||
| const { componentRoute, componentType } = context; | ||
| let shouldCapture = false; | ||
| isolationScope.setTransactionName(`${componentType} Server Component (${componentRoute})`); | ||
|
|
||
| const activeSpan = getActiveSpan(); | ||
| if (activeSpan) { | ||
| const rootSpan = getRootSpan(activeSpan); | ||
| const sentryTrace = headersDict?.['sentry-trace']; | ||
| if (sentryTrace) { | ||
| rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); | ||
| if (span) { | ||
| if (isNotFoundNavigationError(error)) { | ||
| shouldCapture = false; | ||
| // We don't want to report "not-found"s | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); | ||
| } else if (isRedirectNavigationError(error)) { | ||
| shouldCapture = false; | ||
| // We don't want to report redirects | ||
| span.setStatus({ code: SPAN_STATUS_OK }); | ||
| } else { | ||
| span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); | ||
| } | ||
| } | ||
|
|
||
| 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, | ||
| if (shouldCapture) { | ||
| captureException(error, { | ||
|
Comment on lines
+53
to
+63
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 🔍 Detailed AnalysisThe 💡 Suggested FixIn the 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| mechanism: { | ||
| handled: false, | ||
| type: 'auto.function.nextjs.server_component', | ||
| }, | ||
| }, | ||
| 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', | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| () => { | ||
| span.end(); | ||
| waitUntil(flushSafelyWithTimeout()); | ||
| }, | ||
| ); | ||
| }, | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
|
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: Exceptions never captured due to unset shouldCapture flagThe |
||
| }, | ||
| () => { | ||
| waitUntil(flushSafelyWithTimeout()); | ||
| }, | ||
| ); | ||
|
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: Missing isolation scope context breaks request metadata attachmentThe refactored code retrieves an isolation scope via
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. the scope already gets forked earlier
Comment on lines
+64
to
+74
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 created isolation scope is not activated with 🔍 Detailed AnalysisAn isolation scope is created and enriched with metadata at the beginning of 💡 Suggested FixWrap the execution of the server component's callback within 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| }, | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Debug console.log left in test code
A
console.log(transactionEvent?.transaction)statement appears to have been left in the test code, likely from debugging. While this is in test code, it will produce unnecessary output during test runs.