Skip to content

Commit 53915b0

Browse files
authored
feat(nextjs): Connect trace between data-fetching methods and pageload (#5655)
1 parent e6e89fd commit 53915b0

18 files changed

+314
-100
lines changed

packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { serializeBaggage } from '@sentry/utils';
23
import { GetServerSideProps } from 'next';
34

45
import { isBuild } from '../../utils/isBuild';
5-
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';
67

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

3031
if (hasTracingEnabled()) {
31-
return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, {
32-
dataFetcherRouteName: parameterizedRoute,
33-
requestedRouteName: parameterizedRoute,
34-
dataFetchingMethodName: 'getServerSideProps',
35-
});
32+
const serverSideProps = await callTracedServerSideDataFetcher(
33+
errorWrappedGetServerSideProps,
34+
getServerSidePropsArguments,
35+
req,
36+
res,
37+
{
38+
dataFetcherRouteName: parameterizedRoute,
39+
requestedRouteName: parameterizedRoute,
40+
dataFetchingMethodName: 'getServerSideProps',
41+
},
42+
);
43+
44+
if ('props' in serverSideProps) {
45+
const requestTransaction = getTransactionFromRequest(req);
46+
if (requestTransaction) {
47+
serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent();
48+
serverSideProps.props._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
49+
}
50+
}
51+
52+
return serverSideProps;
3653
} else {
3754
return errorWrappedGetServerSideProps(...getServerSidePropsArguments);
3855
}

packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { serializeBaggage } from '@sentry/utils';
23
import App from 'next/app';
34

45
import { isBuild } from '../../utils/isBuild';
5-
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';
67

78
type AppGetInitialProps = typeof App['getInitialProps'];
89

@@ -30,12 +31,30 @@ export function withSentryServerSideAppGetInitialProps(origAppGetInitialProps: A
3031
if (hasTracingEnabled()) {
3132
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
3233
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
33-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
34-
return callTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, appGetInitialPropsArguments, req!, res!, {
35-
dataFetcherRouteName: '/_app',
36-
requestedRouteName: context.ctx.pathname,
37-
dataFetchingMethodName: 'getInitialProps',
38-
});
34+
const appGetInitialProps: {
35+
pageProps: {
36+
_sentryTraceData?: string;
37+
_sentryBaggage?: string;
38+
};
39+
} = await callTracedServerSideDataFetcher(
40+
errorWrappedAppGetInitialProps,
41+
appGetInitialPropsArguments,
42+
req!,
43+
res!,
44+
{
45+
dataFetcherRouteName: '/_app',
46+
requestedRouteName: context.ctx.pathname,
47+
dataFetchingMethodName: 'getInitialProps',
48+
},
49+
);
50+
51+
const requestTransaction = getTransactionFromRequest(req!);
52+
if (requestTransaction) {
53+
appGetInitialProps.pageProps._sentryTraceData = requestTransaction.toTraceparent();
54+
appGetInitialProps.pageProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
55+
}
56+
57+
return appGetInitialProps;
3958
} else {
4059
return errorWrappedAppGetInitialProps(...appGetInitialPropsArguments);
4160
}

packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { serializeBaggage } from '@sentry/utils';
23
import { NextPageContext } from 'next';
34
import { ErrorProps } from 'next/error';
45

56
import { isBuild } from '../../utils/isBuild';
6-
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
7+
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';
78

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

@@ -33,12 +34,28 @@ export function withSentryServerSideErrorGetInitialProps(
3334
if (hasTracingEnabled()) {
3435
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
3536
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
36-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
37-
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, errorGetInitialPropsArguments, req!, res!, {
38-
dataFetcherRouteName: '/_error',
39-
requestedRouteName: context.pathname,
40-
dataFetchingMethodName: 'getInitialProps',
41-
});
37+
const errorGetInitialProps: ErrorProps & {
38+
_sentryTraceData?: string;
39+
_sentryBaggage?: string;
40+
} = await callTracedServerSideDataFetcher(
41+
errorWrappedGetInitialProps,
42+
errorGetInitialPropsArguments,
43+
req!,
44+
res!,
45+
{
46+
dataFetcherRouteName: '/_error',
47+
requestedRouteName: context.pathname,
48+
dataFetchingMethodName: 'getInitialProps',
49+
},
50+
);
51+
52+
const requestTransaction = getTransactionFromRequest(req!);
53+
if (requestTransaction) {
54+
errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent();
55+
errorGetInitialProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
56+
}
57+
58+
return errorGetInitialProps;
4259
} else {
4360
return errorWrappedGetInitialProps(...errorGetInitialPropsArguments);
4461
}

packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { hasTracingEnabled } from '@sentry/tracing';
2+
import { serializeBaggage } from '@sentry/utils';
23
import { NextPage } from 'next';
34

45
import { isBuild } from '../../utils/isBuild';
5-
import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils';
6+
import { callTracedServerSideDataFetcher, getTransactionFromRequest, withErrorInstrumentation } from './wrapperUtils';
67

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

@@ -29,12 +30,22 @@ export function withSentryServerSideGetInitialProps(origGetInitialProps: GetInit
2930
if (hasTracingEnabled()) {
3031
// Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and
3132
// `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
32-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
33-
return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
33+
const initialProps: {
34+
_sentryTraceData?: string;
35+
_sentryBaggage?: string;
36+
} = await callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, {
3437
dataFetcherRouteName: context.pathname,
3538
requestedRouteName: context.pathname,
3639
dataFetchingMethodName: 'getInitialProps',
3740
});
41+
42+
const requestTransaction = getTransactionFromRequest(req!);
43+
if (requestTransaction) {
44+
initialProps._sentryTraceData = requestTransaction.toTraceparent();
45+
initialProps._sentryBaggage = serializeBaggage(requestTransaction.getBaggage());
46+
}
47+
48+
return initialProps;
3849
} else {
3950
return errorWrappedGetInitialProps(...getInitialPropsArguments);
4051
}

packages/nextjs/src/config/wrappers/wrapperUtils.ts

+22-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { captureException, getCurrentHub, startTransaction } from '@sentry/core'
22
import { addRequestDataToEvent } from '@sentry/node';
33
import { getActiveTransaction } from '@sentry/tracing';
44
import { Transaction } from '@sentry/types';
5-
import { fill } from '@sentry/utils';
5+
import { extractTraceparentData, fill, isString, parseBaggageSetMutability } from '@sentry/utils';
66
import * as domain from 'domain';
77
import { IncomingMessage, ServerResponse } from 'http';
88

@@ -12,7 +12,14 @@ declare module 'http' {
1212
}
1313
}
1414

15-
function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
15+
/**
16+
* Grabs a transaction off a Next.js datafetcher request object, if it was previously put there via
17+
* `setTransactionOnRequest`.
18+
*
19+
* @param req The Next.js datafetcher request object
20+
* @returns the Transaction on the request object if there is one, or `undefined` if the request object didn't have one.
21+
*/
22+
export function getTransactionFromRequest(req: IncomingMessage): Transaction | undefined {
1623
return req._sentryTransaction;
1724
}
1825

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

3239
/**
3340
* Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown.
41+
*
42+
* Note: This function turns the wrapped function into an asynchronous one.
3443
*/
3544
export function withErrorInstrumentation<F extends (...args: any[]) => any>(
3645
origFunction: F,
37-
): (...params: Parameters<F>) => ReturnType<F> {
38-
return function (this: unknown, ...origFunctionArguments: Parameters<F>): ReturnType<F> {
46+
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
47+
return async function (this: unknown, ...origFunctionArguments: Parameters<F>): Promise<ReturnType<F>> {
3948
try {
40-
const potentialPromiseResult = origFunction.call(this, ...origFunctionArguments);
41-
// First of all, we need to capture promise rejections so we have the following check, as well as the try-catch block.
42-
// 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>.
43-
Promise.resolve(potentialPromiseResult).catch(err => {
44-
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
45-
captureException(err);
46-
});
47-
return potentialPromiseResult;
49+
return await origFunction.call(this, ...origFunctionArguments);
4850
} catch (e) {
4951
// TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that.
5052
captureException(e);
@@ -85,13 +87,20 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
8587
let requestTransaction: Transaction | undefined = getTransactionFromRequest(req);
8688

8789
if (requestTransaction === undefined) {
88-
// TODO: Extract trace data from `req` object (trace and baggage headers) and attach it to transaction
90+
const sentryTraceHeader = req.headers['sentry-trace'];
91+
const rawBaggageString = req.headers && isString(req.headers.baggage) && req.headers.baggage;
92+
const traceparentData =
93+
typeof sentryTraceHeader === 'string' ? extractTraceparentData(sentryTraceHeader) : undefined;
94+
95+
const baggage = parseBaggageSetMutability(rawBaggageString, traceparentData);
8996

9097
const newTransaction = startTransaction({
9198
op: 'nextjs.data.server',
9299
name: options.requestedRouteName,
100+
...traceparentData,
93101
metadata: {
94102
source: 'route',
103+
baggage,
95104
},
96105
});
97106

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

123132
try {
124-
// TODO: Inject trace data into returned props
125133
return await origFunction(...origFunctionArguments);
126134
} finally {
127135
dataFetcherSpan.finish();

packages/nextjs/src/performance/client.ts

+11-27
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,14 @@ type StartTransactionCb = (context: TransactionContext) => Transaction | undefin
2222
* Describes data located in the __NEXT_DATA__ script tag. This tag is present on every page of a Next.js app.
2323
*/
2424
interface SentryEnhancedNextData extends NextData {
25-
// contains props returned by `getInitialProps` - except for `pageProps`, these are the props that got returned by `getServerSideProps` or `getStaticProps`
2625
props: {
27-
_sentryGetInitialPropsTraceData?: string; // trace parent info, if injected by server-side `getInitialProps`
28-
_sentryGetInitialPropsBaggage?: string; // baggage, if injected by server-side `getInitialProps`
2926
pageProps?: {
30-
_sentryGetServerSidePropsTraceData?: string; // trace parent info, if injected by server-side `getServerSideProps`
31-
_sentryGetServerSidePropsBaggage?: string; // baggage, if injected by server-side `getServerSideProps`
32-
33-
// The following two values are only injected in a very special case with the following conditions:
27+
_sentryTraceData?: string; // trace parent info, if injected by a data-fetcher
28+
_sentryBaggage?: string; // baggage, if injected by a data-fetcher
29+
// These two values are only injected by `getStaticProps` in a very special case with the following conditions:
3430
// 1. The page's `getStaticPaths` method must have returned `fallback: 'blocking'`.
3531
// 2. The requested page must be a "miss" in terms of "Incremental Static Regeneration", meaning the requested page has not been generated before.
3632
// In this case, a page is requested and only served when `getStaticProps` is done. There is not even a fallback page or similar.
37-
_sentryGetStaticPropsTraceData?: string; // trace parent info, if injected by server-side `getStaticProps`
38-
_sentryGetStaticPropsBaggage?: string; // baggage, if injected by server-side `getStaticProps`
3933
};
4034
};
4135
}
@@ -79,29 +73,19 @@ function extractNextDataTagInformation(): NextDataTagInfo {
7973

8074
const { page, query, props } = nextData;
8175

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

86-
if (props) {
87-
const { pageProps } = props;
88-
89-
const getInitialPropsBaggage = props._sentryGetInitialPropsBaggage;
90-
const getServerSidePropsBaggage = pageProps && pageProps._sentryGetServerSidePropsBaggage;
91-
const getStaticPropsBaggage = pageProps && pageProps._sentryGetStaticPropsBaggage;
92-
// Ordering of the following shouldn't matter but `getInitialProps` generally runs before `getServerSideProps` or `getStaticProps` so we give it priority.
93-
const baggage = getInitialPropsBaggage || getServerSidePropsBaggage || getStaticPropsBaggage;
94-
if (baggage) {
95-
nextDataTagInfo.baggage = baggage;
82+
if (props && props.pageProps) {
83+
if (props.pageProps._sentryBaggage) {
84+
nextDataTagInfo.baggage = props.pageProps._sentryBaggage;
9685
}
9786

98-
const getInitialPropsTraceData = props._sentryGetInitialPropsTraceData;
99-
const getServerSidePropsTraceData = pageProps && pageProps._sentryGetServerSidePropsTraceData;
100-
const getStaticPropsTraceData = pageProps && pageProps._sentryGetStaticPropsTraceData;
101-
// Ordering of the following shouldn't matter but `getInitialProps` generally runs before `getServerSideProps` or `getStaticProps` so we give it priority.
102-
const traceData = getInitialPropsTraceData || getServerSidePropsTraceData || getStaticPropsTraceData;
103-
if (traceData) {
104-
nextDataTagInfo.traceParentData = extractTraceparentData(traceData);
87+
if (props.pageProps._sentryTraceData) {
88+
nextDataTagInfo.traceParentData = extractTraceparentData(props.pageProps._sentryTraceData);
10589
}
10690
}
10791

packages/nextjs/test/integration/next.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const moduleExports = {
44
eslint: {
55
ignoreDuringBuilds: true,
66
},
7+
sentry: {
8+
experiments: { autoWrapDataFetchers: true },
9+
},
710
};
811
const SentryWebpackPluginOptions = {
912
dryRun: true,

packages/nextjs/test/integration/next10.config.template

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const moduleExports = {
55
future: {
66
webpack5: %RUN_WEBPACK_5%,
77
},
8+
sentry: {
9+
experiments: { autoWrapDataFetchers: true },
10+
},
811
};
912

1013
const SentryWebpackPluginOptions = {

packages/nextjs/test/integration/next11.config.template

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const moduleExports = {
66
eslint: {
77
ignoreDuringBuilds: true,
88
},
9+
sentry: {
10+
experiments: { autoWrapDataFetchers: true },
11+
},
912
};
1013

1114
const SentryWebpackPluginOptions = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const WithInitialPropsPage = ({ data }: { data: string }) => <h1>WithInitialPropsPage {data}</h1>;
2+
3+
WithInitialPropsPage.getInitialProps = () => {
4+
return { data: '[some getInitialProps data]' };
5+
};
6+
7+
export default WithInitialPropsPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const WithServerSidePropsPage = ({ data }: { data: string }) => <h1>WithServerSidePropsPage {data}</h1>;
2+
3+
export async function getServerSideProps() {
4+
return { props: { data: '[some getServerSideProps data]' } };
5+
}
6+
7+
export default WithServerSidePropsPage;

0 commit comments

Comments
 (0)