Skip to content
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { hasTracingEnabled } from '@sentry/tracing';
import { serializeBaggage } from '@sentry/utils';
import { GetServerSideProps } from 'next';

import { isBuild } from '../../utils/isBuild';
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';

/**
* Create a wrapped version of the user's exported `getServerSideProps` function
Expand All @@ -28,11 +29,27 @@ export function withSentryGetServerSideProps(
const errorWrappedGetServerSideProps = withErrorInstrumentation(origGetServerSideProps);

if (hasTracingEnabled()) {
return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, {
dataFetcherRouteName: parameterizedRoute,
requestedRouteName: parameterizedRoute,
dataFetchingMethodName: 'getServerSideProps',
});
const serverSideProps = await callTracedServerSideDataFetcher(
errorWrappedGetServerSideProps,
getServerSidePropsArguments,
req,
res,
{
dataFetcherRouteName: parameterizedRoute,
requestedRouteName: parameterizedRoute,
dataFetchingMethodName: 'getServerSideProps',
},
);

if ('props' in serverSideProps) {
const requestTransaction = getTransactionFromRequest(req);
if (requestTransaction) {
serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent();
serverSideProps.props._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
}
}

return serverSideProps;
} else {
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { hasTracingEnabled } from '@sentry/tracing';
import { serializeBaggage } from '@sentry/utils';
import App from 'next/app';

import { isBuild } from '../../utils/isBuild';
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';

type AppGetInitialProps = typeof App['getInitialProps'];

Expand Down Expand Up @@ -30,12 +31,30 @@ export function withSentryServerSideAppGetInitialProps(origAppGetInitialProps: A
if (hasTracingEnabled()) {
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return callTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, appGetInitialPropsArguments, req!, res!, {
dataFetcherRouteName: '/_app',
requestedRouteName: context.ctx.pathname,
dataFetchingMethodName: 'getInitialProps',
});
const appGetInitialProps: {
pageProps: {
_sentryTraceData?: string;
_sentryBaggage?: string;
};
} = await callTracedServerSideDataFetcher(
errorWrappedAppGetInitialProps,
appGetInitialPropsArguments,
req!,
res!,
{
dataFetcherRouteName: '/_app',
requestedRouteName: context.ctx.pathname,
dataFetchingMethodName: 'getInitialProps',
},
);

const requestTransaction = getTransactionFromRequest(req!);
if (requestTransaction) {
appGetInitialProps.pageProps._sentryTraceData = requestTransaction.toTraceparent();
appGetInitialProps.pageProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
}

return appGetInitialProps;
} else {
return errorWrappedAppGetInitialProps(...appGetInitialPropsArguments);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { hasTracingEnabled } from '@sentry/tracing';
import { serializeBaggage } from '@sentry/utils';
import { NextPageContext } from 'next';
import { ErrorProps } from 'next/error';

import { isBuild } from '../../utils/isBuild';
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';

type ErrorGetInitialProps = (context: NextPageContext) => Promise<ErrorProps>;

Expand Down Expand Up @@ -33,12 +34,28 @@ export function withSentryServerSideErrorGetInitialProps(
if (hasTracingEnabled()) {
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, errorGetInitialPropsArguments, req!, res!, {
dataFetcherRouteName: '/_error',
requestedRouteName: context.pathname,
dataFetchingMethodName: 'getInitialProps',
});
const errorGetInitialProps: ErrorProps & {
_sentryTraceData?: string;
_sentryBaggage?: string;
} = await callTracedServerSideDataFetcher(
errorWrappedGetInitialProps,
errorGetInitialPropsArguments,
req!,
res!,
{
dataFetcherRouteName: '/_error',
requestedRouteName: context.pathname,
dataFetchingMethodName: 'getInitialProps',
},
);

const requestTransaction = getTransactionFromRequest(req!);
if (requestTransaction) {
errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent();
errorGetInitialProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
}

return errorGetInitialProps;
} else {
return errorWrappedGetInitialProps(...errorGetInitialPropsArguments);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { hasTracingEnabled } from '@sentry/tracing';
import { serializeBaggage } from '@sentry/utils';
import { NextPage } from 'next';

import { isBuild } from '../../utils/isBuild';
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';

type GetInitialProps = Required<NextPage>['getInitialProps'];

Expand All @@ -29,12 +30,22 @@ export function withSentryServerSideGetInitialProps(origGetInitialProps: GetInit
if (hasTracingEnabled()) {
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
const initialProps: {
_sentryTraceData?: string;
_sentryBaggage?: string;
} = await callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
dataFetcherRouteName: context.pathname,
requestedRouteName: context.pathname,
dataFetchingMethodName: 'getInitialProps',
});

const requestTransaction = getTransactionFromRequest(req!);
if (requestTransaction) {
initialProps._sentryTraceData = requestTransaction.toTraceparent();
initialProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
}

return initialProps;
} else {
return errorWrappedGetInitialProps(...getInitialPropsArguments);
}
Expand Down
36 changes: 22 additions & 14 deletions packages/nextjs/src/config/wrappers/wrapperUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { captureException, getCurrentHub, startTransaction } from '@sentry/core'
import { addRequestDataToEvent } from '@sentry/node';
import { getActiveTransaction } from '@sentry/tracing';
import { Transaction } from '@sentry/types';
import { fill } from '@sentry/utils';
import { extractTraceparentData, fill, isString, parseBaggageSetMutability } from '@sentry/utils';
import * as domain from 'domain';
import { IncomingMessage, ServerResponse } from 'http';

Expand All @@ -12,7 +12,14 @@ declare module 'http' {
}
}

function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
/**
* Grabs a transaction off a Next.js datafetcher request object, if it was previously put there via
* `setTransactionOnRequest`.
*
* @param req The Next.js datafetcher request object
* @returns the Transaction on the request object if there is one, or `undefined` if the request object didn't have one.
*/
export function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
return req._sentryTransaction;
}
Comment on lines +15 to 24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose to wrap this line in a function? (Same goes for the setter.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know to be honest. It just made sense to me to put this behind a little bit of abstraction to provide more context why this field exists on the req object. Feel free to remove it in the future though! I think the ambient type (declare module 'http' { at the top of the file) provides enough context.


Expand All @@ -31,20 +38,15 @@ function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerRe

/**
* Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown.
*
* Note: This function turns the wrapped function into an asynchronous one.
*/
export function withErrorInstrumentation<F extends (...args: any[]) => any>(
origFunction: F,
): (...params: Parameters<F>) => ReturnType<F> {
return function (this: unknown, ...origFunctionArguments: Parameters<F>): ReturnType<F> {
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
return async function (this: unknown, ...origFunctionArguments: Parameters<F>): Promise<ReturnType<F>> {
try {
const potentialPromiseResult = origFunction.call(this, ...origFunctionArguments);
// First of all, we need to capture promise rejections so we have the following check, as well as the try-catch block.
// Additionally, we do the following instead of `await`-ing so we do not change the method signature of the passed function from `() => unknown` to `() => Promise<unknown>.
Promise.resolve(potentialPromiseResult).catch(err => {
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
captureException(err);
});
return potentialPromiseResult;
return await origFunction.call(this, ...origFunctionArguments);
} catch (e) {
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
captureException(e);
Expand Down Expand Up @@ -85,13 +87,20 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
let requestTransaction: Transaction | undefined = getTransactionFromRequest(req);

if (requestTransaction === undefined) {
// TODO: Extract trace data from `req` object (trace and baggage headers) and attach it to transaction
const sentryTraceHeader = req.headers['sentry-trace'];
const rawBaggageString = req.headers && isString(req.headers.baggage) && req.headers.baggage;
const traceparentData =
typeof sentryTraceHeader === 'string' ? extractTraceparentData(sentryTraceHeader) : undefined;

const baggage = parseBaggageSetMutability(rawBaggageString, traceparentData);

const newTransaction = startTransaction({
op: 'nextjs.data.server',
name: options.requestedRouteName,
...traceparentData,
metadata: {
source: 'route',
baggage,
},
});

Expand Down Expand Up @@ -121,7 +130,6 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
}

try {
// TODO: Inject trace data into returned props
return await origFunction(...origFunctionArguments);
} finally {
dataFetcherSpan.finish();
Expand Down
38 changes: 11 additions & 27 deletions packages/nextjs/src/performance/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,14 @@ type StartTransactionCb = (context: TransactionContext) => Transaction | undefin
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
*/
interface SentryEnhancedNextData extends NextData {
// contains props returned by `getInitialProps` - except for `pageProps`, these are the props that got returned by `getServerSideProps` or `getStaticProps`
props: {
_sentryGetInitialPropsTraceData?: string; // trace parent info, if injected by server-side `getInitialProps`
_sentryGetInitialPropsBaggage?: string; // baggage, if injected by server-side `getInitialProps`
Comment on lines -25 to -28
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this wrong in a previous PR

pageProps?: {
_sentryGetServerSidePropsTraceData?: string; // trace parent info, if injected by server-side `getServerSideProps`
_sentryGetServerSidePropsBaggage?: string; // baggage, if injected by server-side `getServerSideProps`

// The following two values are only injected in a very special case with the following conditions:
_sentryTraceData?: string; // trace parent info, if injected by a data-fetcher
_sentryBaggage?: string; // baggage, if injected by a data-fetcher
// These two values are only injected by `getStaticProps` in a very special case with the following conditions:
// 1. The page's `getStaticPaths` method must have returned `fallback: 'blocking'`.
// 2. The requested page must be a "miss" in terms of "Incremental Static Regeneration", meaning the requested page has not been generated before.
// In this case, a page is requested and only served when `getStaticProps` is done. There is not even a fallback page or similar.
_sentryGetStaticPropsTraceData?: string; // trace parent info, if injected by server-side `getStaticProps`
_sentryGetStaticPropsBaggage?: string; // baggage, if injected by server-side `getStaticProps`
};
};
}
Expand Down Expand Up @@ -79,29 +73,19 @@ function extractNextDataTagInformation(): NextDataTagInfo {

const { page, query, props } = nextData;

// `nextData.page` always contains the parameterized route
// `nextData.page` always contains the parameterized route - except for when an error occurs in a data fetching
// function, then it is "/_error", but that isn't a problem since users know which route threw by looking at the
// parent transaction
nextDataTagInfo.route = page;
nextDataTagInfo.params = query;

if (props) {
const { pageProps } = props;

const getInitialPropsBaggage = props._sentryGetInitialPropsBaggage;
const getServerSidePropsBaggage = pageProps && pageProps._sentryGetServerSidePropsBaggage;
const getStaticPropsBaggage = pageProps && pageProps._sentryGetStaticPropsBaggage;
// Ordering of the following shouldn't matter but `getInitialProps` generally runs before `getServerSideProps` or `getStaticProps` so we give it priority.
const baggage = getInitialPropsBaggage || getServerSidePropsBaggage || getStaticPropsBaggage;
if (baggage) {
nextDataTagInfo.baggage = baggage;
if (props && props.pageProps) {
if (props.pageProps._sentryBaggage) {
nextDataTagInfo.baggage = props.pageProps._sentryBaggage;
}

const getInitialPropsTraceData = props._sentryGetInitialPropsTraceData;
const getServerSidePropsTraceData = pageProps && pageProps._sentryGetServerSidePropsTraceData;
const getStaticPropsTraceData = pageProps && pageProps._sentryGetStaticPropsTraceData;
// Ordering of the following shouldn't matter but `getInitialProps` generally runs before `getServerSideProps` or `getStaticProps` so we give it priority.
const traceData = getInitialPropsTraceData || getServerSidePropsTraceData || getStaticPropsTraceData;
if (traceData) {
nextDataTagInfo.traceParentData = extractTraceparentData(traceData);
if (props.pageProps._sentryTraceData) {
nextDataTagInfo.traceParentData = extractTraceparentData(props.pageProps._sentryTraceData);
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/nextjs/test/integration/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const moduleExports = {
eslint: {
ignoreDuringBuilds: true,
},
sentry: {
experiments: { autoWrapDataFetchers: true },
},
};
const SentryWebpackPluginOptions = {
dryRun: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/nextjs/test/integration/next10.config.template
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const moduleExports = {
future: {
webpack5: %RUN_WEBPACK_5%,
},
sentry: {
experiments: { autoWrapDataFetchers: true },
},
};

const SentryWebpackPluginOptions = {
Expand Down
3 changes: 3 additions & 0 deletions packages/nextjs/test/integration/next11.config.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const moduleExports = {
eslint: {
ignoreDuringBuilds: true,
},
sentry: {
experiments: { autoWrapDataFetchers: true },
},
};

const SentryWebpackPluginOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const WithInitialPropsPage = ({ data }: { data: string }) => <h1>WithInitialPropsPage {data}</h1>;

WithInitialPropsPage.getInitialProps = () => {
return { data: '[some getInitialProps data]' };
};

export default WithInitialPropsPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const WithServerSidePropsPage = ({ data }: { data: string }) => <h1>WithServerSidePropsPage {data}</h1>;

export async function getServerSideProps() {
return { props: { data: '[some getServerSideProps data]' } };
}

export default WithServerSidePropsPage;
Loading