Skip to content

Commit 4d7ac23

Browse files
author
Luca Forstner
authored
fix(nextjs): Continue traces in data fetchers when there is an already active transaction on the hub (#8073)
1 parent 17f11e8 commit 4d7ac23

File tree

6 files changed

+36
-25
lines changed

6 files changed

+36
-25
lines changed

packages/nextjs/src/server/utils/wrapperUtils.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
runWithAsyncContext,
66
startTransaction,
77
} from '@sentry/core';
8-
import type { Transaction } from '@sentry/types';
8+
import type { Span, Transaction } from '@sentry/types';
99
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
1010
import type { IncomingMessage, ServerResponse } from 'http';
1111

@@ -82,7 +82,8 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
8282
return async function (this: unknown, ...args: Parameters<F>): Promise<ReturnType<F>> {
8383
return runWithAsyncContext(async () => {
8484
const hub = getCurrentHub();
85-
let requestTransaction: Transaction | undefined = getTransactionFromRequest(req);
85+
const scope = hub.getScope();
86+
const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan();
8687
let dataFetcherSpan;
8788

8889
const sentryTraceHeader = req.headers['sentry-trace'];
@@ -93,7 +94,8 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
9394
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(rawBaggageString);
9495

9596
if (platformSupportsStreaming()) {
96-
if (requestTransaction === undefined) {
97+
let spanToContinue: Span;
98+
if (previousSpan === undefined) {
9799
const newTransaction = startTransaction(
98100
{
99101
op: 'http.server',
@@ -109,8 +111,6 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
109111
{ request: req },
110112
);
111113

112-
requestTransaction = newTransaction;
113-
114114
if (platformSupportsStreaming()) {
115115
// On platforms that don't support streaming, doing things after res.end() is unreliable.
116116
autoEndTransactionOnResponseEnd(newTransaction, res);
@@ -119,9 +119,12 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
119119
// Link the transaction and the request together, so that when we would normally only have access to one, it's
120120
// still possible to grab the other.
121121
setTransactionOnRequest(newTransaction, req);
122+
spanToContinue = newTransaction;
123+
} else {
124+
spanToContinue = previousSpan;
122125
}
123126

124-
dataFetcherSpan = requestTransaction.startChild({
127+
dataFetcherSpan = spanToContinue.startChild({
125128
op: 'function.nextjs',
126129
description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
127130
status: 'ok',
@@ -140,22 +143,20 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
140143
});
141144
}
142145

143-
const currentScope = hub.getScope();
144-
if (currentScope) {
145-
currentScope.setSpan(dataFetcherSpan);
146-
currentScope.setSDKProcessingMetadata({ request: req });
147-
}
146+
scope.setSpan(dataFetcherSpan);
147+
scope.setSDKProcessingMetadata({ request: req });
148148

149149
try {
150150
return await origDataFetcher.apply(this, args);
151151
} catch (e) {
152152
// Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation`
153153
// that set the transaction status, we need to manually set the status of the span & transaction
154154
dataFetcherSpan.setStatus('internal_error');
155-
requestTransaction?.setStatus('internal_error');
155+
previousSpan?.setStatus('internal_error');
156156
throw e;
157157
} finally {
158158
dataFetcherSpan.finish();
159+
scope.setSpan(previousSpan);
159160
if (!platformSupportsStreaming()) {
160161
await flushQueue();
161162
}

packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
3131
const { req, res } = context.ctx;
3232

3333
const errorWrappedAppGetInitialProps = withErrorInstrumentation(wrappingTarget);
34-
const options = getCurrentHub().getClient()?.getOptions();
34+
const hub = getCurrentHub();
35+
const options = hub.getClient()?.getOptions();
3536

3637
// Generally we can assume that `req` and `res` are always defined on the server:
3738
// https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
@@ -51,7 +52,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
5152
};
5253
} = await tracedGetInitialProps.apply(thisArg, args);
5354

54-
const requestTransaction = getTransactionFromRequest(req);
55+
const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction();
5556

5657
// Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call
5758
// `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per

packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export function wrapErrorGetInitialPropsWithSentry(
3434
const { req, res } = context;
3535

3636
const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget);
37-
const options = getCurrentHub().getClient()?.getOptions();
37+
const hub = getCurrentHub();
38+
const options = hub.getClient()?.getOptions();
3839

3940
// Generally we can assume that `req` and `res` are always defined on the server:
4041
// https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
@@ -52,7 +53,7 @@ export function wrapErrorGetInitialPropsWithSentry(
5253
_sentryBaggage?: string;
5354
} = await tracedGetInitialProps.apply(thisArg, args);
5455

55-
const requestTransaction = getTransactionFromRequest(req);
56+
const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction();
5657
if (requestTransaction) {
5758
errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent();
5859

packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro
3030
const { req, res } = context;
3131

3232
const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget);
33-
const options = getCurrentHub().getClient()?.getOptions();
33+
const hub = getCurrentHub();
34+
const options = hub.getClient()?.getOptions();
3435

3536
// Generally we can assume that `req` and `res` are always defined on the server:
3637
// https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
@@ -48,7 +49,7 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro
4849
_sentryBaggage?: string;
4950
} = await tracedGetInitialProps.apply(thisArg, args);
5051

51-
const requestTransaction = getTransactionFromRequest(req);
52+
const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction();
5253
if (requestTransaction) {
5354
initialProps._sentryTraceData = requestTransaction.toTraceparent();
5455

packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export function wrapGetServerSidePropsWithSentry(
3131
const { req, res } = context;
3232

3333
const errorWrappedGetServerSideProps = withErrorInstrumentation(wrappingTarget);
34-
const options = getCurrentHub().getClient()?.getOptions();
34+
const hub = getCurrentHub();
35+
const options = hub.getClient()?.getOptions();
3536

3637
if (hasTracingEnabled() && options?.instrumenter === 'sentry') {
3738
const tracedGetServerSideProps = withTracedServerSideDataFetcher(errorWrappedGetServerSideProps, req, res, {
@@ -45,7 +46,7 @@ export function wrapGetServerSidePropsWithSentry(
4546
>);
4647

4748
if (serverSideProps && 'props' in serverSideProps) {
48-
const requestTransaction = getTransactionFromRequest(req);
49+
const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction();
4950
if (requestTransaction) {
5051
serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent();
5152

packages/nextjs/test/config/wrappers.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
66
import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/server';
77

88
const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction');
9+
const originalGetCurrentHub = jest.requireActual('@sentry/node').getCurrentHub;
910

1011
// The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient
1112
// constructor but the client isn't used in these tests.
@@ -21,13 +22,18 @@ describe('data-fetching function wrappers', () => {
2122
req = { headers: {}, url: 'http://dogs.are.great/tricks/kangaroo' } as IncomingMessage;
2223
res = { end: jest.fn() } as unknown as ServerResponse;
2324

24-
jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValueOnce(true);
25-
jest.spyOn(SentryNode, 'getCurrentHub').mockReturnValueOnce({
26-
getClient: () =>
25+
jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValue(true);
26+
jest.spyOn(SentryNode, 'getCurrentHub').mockImplementation(() => {
27+
const hub = originalGetCurrentHub();
28+
29+
hub.getClient = () =>
2730
({
2831
getOptions: () => ({ instrumenter: 'sentry' }),
29-
} as any),
30-
} as any);
32+
getDsn: () => {},
33+
} as any);
34+
35+
return hub;
36+
});
3137
});
3238

3339
afterEach(() => {

0 commit comments

Comments
 (0)