Skip to content

Commit 2f6deeb

Browse files
authored
feat(nextjs): Create spans and route parameterization in server-side getInitialProps (#5587)
1 parent e861cc4 commit 2f6deeb

File tree

6 files changed

+119
-119
lines changed

6 files changed

+119
-119
lines changed

packages/nextjs/src/config/loaders/dataFetchersLoader.ts

+37-21
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>,
111111
// We know one or the other will be defined, depending on the version of webpack being used
112112
const { projectDir, pagesDir } = 'getOptions' in this ? this.getOptions() : this.query;
113113

114+
// Get the parameterized route name from this page's filepath
115+
const parameterizedRouteName = path
116+
// Get the path of the file insde of the pages directory
117+
.relative(pagesDir, this.resourcePath)
118+
// Add a slash at the beginning
119+
.replace(/(.*)/, '/$1')
120+
// Pull off the file extension
121+
.replace(/\.(jsx?|tsx?)/, '')
122+
// Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into
123+
// just `/xyz`
124+
.replace(/\/index$/, '')
125+
// In case all of the above have left us with an empty string (which will happen if we're dealing with the
126+
// homepage), sub back in the root route
127+
.replace(/^$/, '/');
128+
114129
// In the following branch we will proxy the user's file. This means we return code (basically an entirely new file)
115130
// that re - exports all the user file's originial export, but with a "sentry-proxy-loader" query in the module
116131
// string.
@@ -136,13 +151,26 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>,
136151
if (hasDefaultExport(ast)) {
137152
outputFileContent += `
138153
import { default as _sentry_default } from "${this.resourcePath}?sentry-proxy-loader";
139-
import { withSentryGetInitialProps } from "@sentry/nextjs";
140-
141-
if (typeof _sentry_default.getInitialProps === 'function') {
142-
_sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps);
143-
}
144-
145-
export default _sentry_default;`;
154+
import { withSentryGetInitialProps } from "@sentry/nextjs";`;
155+
156+
if (parameterizedRouteName === '/_app') {
157+
// getInitialProps signature is a bit different in _app.js so we need a different wrapper
158+
// Currently a no-op
159+
} else if (parameterizedRouteName === '/_error') {
160+
// getInitialProps behaviour is a bit different in _error.js so we probably want different wrapper
161+
// Currently a no-op
162+
} else if (parameterizedRouteName === '/_document') {
163+
// getInitialProps signature is a bit different in _document.js so we need a different wrapper
164+
// Currently a no-op
165+
} else {
166+
// We enter this branch for any "normal" Next.js page
167+
outputFileContent += `
168+
if (typeof _sentry_default.getInitialProps === 'function') {
169+
_sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps, '${parameterizedRouteName}');
170+
}`;
171+
}
172+
173+
outputFileContent += 'export default _sentry_default;';
146174
}
147175

148176
return outputFileContent;
@@ -173,20 +201,8 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>,
173201

174202
// Fill in template placeholders
175203
let injectedCode = modifiedTemplateCode;
176-
const route = path
177-
// Get the path of the file insde of the pages directory
178-
.relative(pagesDir, this.resourcePath)
179-
// Add a slash at the beginning
180-
.replace(/(.*)/, '/$1')
181-
// Pull off the file extension
182-
.replace(/\.(jsx?|tsx?)/, '')
183-
// Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into
184-
// just `/xyz`
185-
.replace(/\/index$/, '')
186-
// In case all of the above have left us with an empty string (which will happen if we're dealing with the
187-
// homepage), sub back in the root route
188-
.replace(/^$/, '/');
189-
injectedCode = injectedCode.replace('__FILEPATH__', route);
204+
205+
injectedCode = injectedCode.replace('__FILEPATH__', parameterizedRouteName);
190206
for (const { placeholder, alias } of Object.values(DATA_FETCHING_FUNCTIONS)) {
191207
injectedCode = injectedCode.replace(new RegExp(placeholder, 'g'), alias);
192208
}

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

-35
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1-
import { GIProps } from './types';
1+
import { NextPage } from 'next';
2+
3+
import { callDataFetcherTraced } from './wrapperUtils';
4+
5+
type GetInitialProps = Required<NextPage<unknown>>['getInitialProps'];
26

37
/**
48
* Create a wrapped version of the user's exported `getInitialProps` function
59
*
6-
* @param origGIProps: The user's `getInitialProps` function
7-
* @param origGIPropsHost: The user's object on which `getInitialProps` lives (used for `this`)
10+
* @param origGetInitialProps The user's `getInitialProps` function
11+
* @param parameterizedRoute The page's parameterized route
812
* @returns A wrapped version of the function
913
*/
10-
export function withSentryGetInitialProps(origGIProps: GIProps['fn']): GIProps['wrappedFn'] {
11-
return async function (this: unknown, ...args: Parameters<GIProps['fn']>) {
12-
return await origGIProps.call(this, ...args);
14+
export function withSentryGetInitialProps(
15+
origGetInitialProps: GetInitialProps,
16+
parameterizedRoute: string,
17+
): GetInitialProps {
18+
return async function (
19+
...getInitialPropsArguments: Parameters<GetInitialProps>
20+
): Promise<ReturnType<GetInitialProps>> {
21+
return callDataFetcherTraced(origGetInitialProps, getInitialPropsArguments, {
22+
parameterizedRoute,
23+
dataFetchingMethodName: 'getInitialProps',
24+
});
1325
};
1426
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
import { GSSP } from './types';
2-
import { wrapperCore } from './wrapperUtils';
1+
import { GetServerSideProps } from 'next';
2+
3+
import { callDataFetcherTraced } from './wrapperUtils';
34

45
/**
56
* Create a wrapped version of the user's exported `getServerSideProps` function
67
*
7-
* @param origGetServerSideProps: The user's `getServerSideProps` function
8-
* @param route: The page's parameterized route
8+
* @param origGetServerSideProps The user's `getServerSideProps` function
9+
* @param parameterizedRoute The page's parameterized route
910
* @returns A wrapped version of the function
1011
*/
11-
export function withSentryGetServerSideProps(origGetServerSideProps: GSSP['fn'], route: string): GSSP['wrappedFn'] {
12-
return async function (context: GSSP['context']): Promise<GSSP['result']> {
13-
return wrapperCore<GSSP>(origGetServerSideProps, context, route);
12+
export function withSentryGetServerSideProps(
13+
origGetServerSideProps: GetServerSideProps,
14+
parameterizedRoute: string,
15+
): GetServerSideProps {
16+
return async function (
17+
...getServerSidePropsArguments: Parameters<GetServerSideProps>
18+
): ReturnType<GetServerSideProps> {
19+
return callDataFetcherTraced(origGetServerSideProps, getServerSidePropsArguments, {
20+
parameterizedRoute,
21+
dataFetchingMethodName: 'getServerSideProps',
22+
});
1423
};
1524
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
import { GSProps } from './types';
2-
import { wrapperCore } from './wrapperUtils';
1+
import { GetStaticProps } from 'next';
2+
3+
import { callDataFetcherTraced } from './wrapperUtils';
4+
5+
type Props = { [key: string]: unknown };
36

47
/**
58
* Create a wrapped version of the user's exported `getStaticProps` function
69
*
7-
* @param origGetStaticProps: The user's `getStaticProps` function
8-
* @param route: The page's parameterized route
10+
* @param origGetStaticProps The user's `getStaticProps` function
11+
* @param parameterizedRoute The page's parameterized route
912
* @returns A wrapped version of the function
1013
*/
11-
export function withSentryGetStaticProps(origGetStaticProps: GSProps['fn'], route: string): GSProps['wrappedFn'] {
12-
return async function (context: GSProps['context']): Promise<GSProps['result']> {
13-
return wrapperCore<GSProps>(origGetStaticProps, context, route);
14+
export function withSentryGetStaticProps(
15+
origGetStaticProps: GetStaticProps<Props>,
16+
parameterizedRoute: string,
17+
): GetStaticProps<Props> {
18+
return async function (
19+
...getStaticPropsArguments: Parameters<GetStaticProps<Props>>
20+
): ReturnType<GetStaticProps<Props>> {
21+
return callDataFetcherTraced(origGetStaticProps, getStaticPropsArguments, {
22+
parameterizedRoute,
23+
dataFetchingMethodName: 'getStaticProps',
24+
});
1425
};
1526
}

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

+30-43
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,45 @@
11
import { captureException } from '@sentry/core';
22
import { getActiveTransaction } from '@sentry/tracing';
3-
import { Span } from '@sentry/types';
4-
5-
import { DataFetchingFunction } from './types';
63

74
/**
8-
* Create a span to track the wrapped function and update transaction name with parameterized route.
5+
* Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope.
96
*
10-
* @template T Types for `getInitialProps`, `getStaticProps`, and `getServerSideProps`
11-
* @param origFunction The user's exported `getInitialProps`, `getStaticProps`, or `getServerSideProps` function
12-
* @param context The context object passed by nextjs to the function
13-
* @param route The route currently being served
14-
* @returns The result of calling the user's function
7+
* We only do the following until we move transaction creation into this function: When called, the wrapped function
8+
* will also update the name of the active transaction with a parameterized route provided via the `options` argument.
159
*/
16-
export async function wrapperCore<T extends DataFetchingFunction>(
17-
origFunction: T['fn'],
18-
context: T['context'],
19-
route: string,
20-
): Promise<T['result']> {
21-
const transaction = getActiveTransaction();
22-
23-
if (transaction) {
24-
// Pull off any leading underscores we've added in the process of wrapping the function
25-
const wrappedFunctionName = origFunction.name.replace(/^_*/, '');
26-
27-
// TODO: Make sure that the given route matches the name of the active transaction (to prevent background data
28-
// fetching from switching the name to a completely other route)
29-
transaction.name = route;
30-
transaction.metadata.source = 'route';
10+
export async function callDataFetcherTraced<F extends (...args: any[]) => Promise<any> | any>(
11+
origFunction: F,
12+
origFunctionArgs: Parameters<F>,
13+
options: {
14+
parameterizedRoute: string;
15+
dataFetchingMethodName: string;
16+
},
17+
): Promise<ReturnType<F>> {
18+
const { parameterizedRoute, dataFetchingMethodName } = options;
3119

32-
// Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another
33-
// route's transaction
34-
const span = transaction.startChild({ op: 'nextjs.data', description: `${wrappedFunctionName} (${route})` });
35-
36-
const props = await callOriginal(origFunction, context, span);
37-
38-
span.finish();
20+
const transaction = getActiveTransaction();
3921

40-
return props;
22+
if (!transaction) {
23+
return origFunction(...origFunctionArgs);
4124
}
4225

43-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
44-
return callOriginal(origFunction, context);
45-
}
26+
// TODO: Make sure that the given route matches the name of the active transaction (to prevent background data
27+
// fetching from switching the name to a completely other route) -- We'll probably switch to creating a transaction
28+
// right here so making that check will probabably not even be necessary.
29+
// Logic will be: If there is no active transaction, start one with correct name and source. If there is an active
30+
// transaction, create a child span with correct name and source.
31+
transaction.name = parameterizedRoute;
32+
transaction.metadata.source = 'route';
33+
34+
// Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another
35+
// route's transaction
36+
const span = transaction.startChild({
37+
op: 'nextjs.data',
38+
description: `${dataFetchingMethodName} (${parameterizedRoute})`,
39+
});
4640

47-
/** Call the original function, capturing any errors and finishing the span (if any) in case of error */
48-
async function callOriginal<T extends DataFetchingFunction>(
49-
origFunction: T['fn'],
50-
context: T['context'],
51-
span?: Span,
52-
): Promise<T['result']> {
5341
try {
54-
// eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any
55-
return (origFunction as any)(context);
42+
return await origFunction(...origFunctionArgs);
5643
} catch (err) {
5744
if (span) {
5845
span.finish();

0 commit comments

Comments
 (0)