Skip to content

Commit

Permalink
feat(nextjs): Trace errors in page components
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Oct 27, 2023
1 parent 8b7b81e commit 5ba7ec0
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -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();
});
94 changes: 65 additions & 29 deletions packages/nextjs/src/common/wrapPageComponentWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { captureException } from '@sentry/core';
import { addExceptionMechanism } from '@sentry/utils';
import { captureException, configureScope, runWithAsyncContext } from '@sentry/core';
import { addExceptionMechanism, extractTraceparentData } from '@sentry/utils';

interface FunctionComponent {
(...args: unknown[]): unknown;
}

interface ClassComponent {
new (...args: unknown[]): {
props?: unknown;
render(...args: unknown[]): unknown;
};
}
Expand All @@ -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 {
Expand Down

0 comments on commit 5ba7ec0

Please sign in to comment.