diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts index 2c1eb9729fdd..9401fad501f5 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -1,24 +1,38 @@ import { test, expect } from '@playwright/test'; -import { waitForError } from '../event-proxy-server'; +import { waitForError, waitForTransaction } from '../event-proxy-server'; -test('Will capture error for SSR rendering error (Class Component)', async ({ page }) => { +test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class'; }); + const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === '/pages-router/ssr-error-class' && + (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + await page.goto('/pages-router/ssr-error-class'); - const errorEvent = await errorEventPromise; - expect(errorEvent).toBeDefined(); + expect(await errorEventPromise).toBeDefined(); + expect(await serverComponentTransaction).toBeDefined(); }); -test('Will capture error for SSR rendering error (Functional Component)', async ({ page }) => { +test('Will capture error for SSR rendering error with a connected trace (Functional Component)', async ({ page }) => { const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC'; }); + const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === '/pages-router/ssr-error-class' && + (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + await page.goto('/pages-router/ssr-error-fc'); - const errorEvent = await errorEventPromise; - expect(errorEvent).toBeDefined(); + expect(await errorEventPromise).toBeDefined(); + expect(await serverComponentTransaction).toBeDefined(); }); diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts index d67dd2a544c0..ddfa6e0da228 100644 --- a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -1,5 +1,5 @@ -import { captureException } from '@sentry/core'; -import { addExceptionMechanism } from '@sentry/utils'; +import { captureException, configureScope, runWithAsyncContext, Scope, trace, withScope } from '@sentry/core'; +import { addExceptionMechanism, extractTraceparentData } from '@sentry/utils'; interface FunctionComponent { (...args: unknown[]): unknown; @@ -7,6 +7,7 @@ interface FunctionComponent { interface ClassComponent { new (...args: unknown[]): { + props?: unknown; render(...args: unknown[]): unknown; }; } @@ -23,41 +24,76 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C if (isReactClassComponent(pageComponent)) { return class SentryWrappedPageComponent extends pageComponent { public render(...args: unknown[]): unknown { - try { - return super.render(...args); - } catch (e) { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); + return runWithAsyncContext(() => { + configureScope(scope => { + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = + typeof this.props === 'object' && + this.props !== null && + '_sentryTraceData' in this.props && + typeof this.props._sentryTraceData === 'string' + ? this.props._sentryTraceData + : undefined; - return scope; + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } }); - throw e; - } + + try { + return super.render(...args); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + }); } }; } else if (typeof pageComponent === 'function') { return new Proxy(pageComponent, { - apply(target, thisArg, argArray) { - try { - return target.apply(thisArg, argArray); - } catch (e) { - captureException(e, scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - handled: false, - }); - return event; - }); + apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) { + return runWithAsyncContext(() => { + configureScope(scope => { + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = argArray?.[0]?._sentryTraceData; - return scope; + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } }); - throw e; - } + try { + return target.apply(thisArg, argArray); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + }); }, }); } else {