Skip to content

Commit

Permalink
feat(nextjs): Always transmit trace data to the client (#13337)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Aug 13, 2024
1 parent 3871892 commit 043ae7e
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 100 deletions.
15 changes: 12 additions & 3 deletions packages/nextjs/src/common/utils/wrapperUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SPAN_STATUS_OK,
captureException,
continueTrace,
getTraceData,
startInactiveSpan,
startSpan,
startSpanManual,
Expand Down Expand Up @@ -88,8 +89,11 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
/** Name of the data fetching method - will be used for describing the data fetcher's span. */
dataFetchingMethodName: string;
},
): (...params: Parameters<F>) => Promise<ReturnType<F>> {
return async function (this: unknown, ...args: Parameters<F>): Promise<ReturnType<F>> {
): (...params: Parameters<F>) => Promise<{ data: ReturnType<F>; sentryTrace?: string; baggage?: string }> {
return async function (
this: unknown,
...args: Parameters<F>
): Promise<{ data: ReturnType<F>; sentryTrace?: string; baggage?: string }> {
return escapeNextjsTracing(() => {
const isolationScope = commonObjectToIsolationScope(req);
return withIsolationScope(isolationScope, () => {
Expand All @@ -116,8 +120,13 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
},
async dataFetcherSpan => {
dataFetcherSpan.setStatus({ code: SPAN_STATUS_OK });
const { 'sentry-trace': sentryTrace, baggage } = getTraceData();
try {
return await origDataFetcher.apply(this, args);
return {
sentryTrace: sentryTrace,
baggage: baggage,
data: await origDataFetcher.apply(this, args),
};
} catch (e) {
dataFetcherSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
requestSpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
Expand Down
45 changes: 21 additions & 24 deletions packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { getActiveSpan, getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
import type App from 'next/app';

import { isBuild } from './utils/isBuild';
import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';

type AppGetInitialProps = (typeof App)['getInitialProps'];

Expand All @@ -26,6 +24,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
const { req, res } = context.ctx;

const errorWrappedAppGetInitialProps = withErrorInstrumentation(wrappingTarget);

// Generally we can assume that `req` and `res` are always defined on the server:
// https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object
// This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher
Expand All @@ -37,16 +36,21 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
dataFetchingMethodName: 'getInitialProps',
});

const appGetInitialProps: {
pageProps: {
_sentryTraceData?: string;
_sentryBaggage?: string;
const {
data: appGetInitialProps,
sentryTrace,
baggage,
}: {
data: {
pageProps: {
_sentryTraceData?: string;
_sentryBaggage?: string;
};
};
sentryTrace?: string;
baggage?: string;
} = await tracedGetInitialProps.apply(thisArg, args);

const activeSpan = getActiveSpan();
const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined);

// Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call
// `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per
// https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`.
Expand All @@ -55,21 +59,14 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI
appGetInitialProps.pageProps = {};
}

if (requestSpan) {
const sentryTrace = spanToTraceHeader(requestSpan);

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
appGetInitialProps.pageProps._sentryTraceData = sentryTrace;
}

const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan);
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
appGetInitialProps.pageProps._sentryTraceData = sentryTrace;
}

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
appGetInitialProps.pageProps._sentryBaggage = baggage;
}
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
appGetInitialProps.pageProps._sentryBaggage = baggage;
}

return appGetInitialProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function wrapDocumentGetInitialPropsWithSentry(
origDocumentGetInitialProps: DocumentGetInitialProps,
): DocumentGetInitialProps {
return new Proxy(origDocumentGetInitialProps, {
apply: (wrappingTarget, thisArg, args: Parameters<DocumentGetInitialProps>) => {
apply: async (wrappingTarget, thisArg, args: Parameters<DocumentGetInitialProps>) => {
if (isBuild()) {
return wrappingTarget.apply(thisArg, args);
}
Expand All @@ -37,7 +37,8 @@ export function wrapDocumentGetInitialPropsWithSentry(
dataFetchingMethodName: 'getInitialProps',
});

return tracedGetInitialProps.apply(thisArg, args);
const { data } = await tracedGetInitialProps.apply(thisArg, args);
return data;
} else {
return errorWrappedGetInitialProps.apply(thisArg, args);
}
Expand Down
42 changes: 19 additions & 23 deletions packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { getActiveSpan, getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
import type { NextPageContext } from 'next';
import type { ErrorProps } from 'next/error';

import { isBuild } from './utils/isBuild';
import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';

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

Expand Down Expand Up @@ -40,29 +38,27 @@ export function wrapErrorGetInitialPropsWithSentry(
dataFetchingMethodName: 'getInitialProps',
});

const errorGetInitialProps: ErrorProps & {
_sentryTraceData?: string;
_sentryBaggage?: string;
const {
data: errorGetInitialProps,
baggage,
sentryTrace,
}: {
data: ErrorProps & {
_sentryTraceData?: string;
_sentryBaggage?: string;
};
baggage?: string;
sentryTrace?: string;
} = await tracedGetInitialProps.apply(thisArg, args);

const activeSpan = getActiveSpan();
const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined);

if (requestSpan) {
const sentryTrace = spanToTraceHeader(requestSpan);

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
errorGetInitialProps._sentryTraceData = sentryTrace;
}

const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan);
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
errorGetInitialProps._sentryTraceData = sentryTrace;
}

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
errorGetInitialProps._sentryBaggage = baggage;
}
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
errorGetInitialProps._sentryBaggage = baggage;
}

return errorGetInitialProps;
Expand Down
42 changes: 19 additions & 23 deletions packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { getActiveSpan, getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
import type { NextPage } from 'next';

import { isBuild } from './utils/isBuild';
import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';

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

Expand Down Expand Up @@ -36,29 +34,27 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro
dataFetchingMethodName: 'getInitialProps',
});

const initialProps: {
_sentryTraceData?: string;
_sentryBaggage?: string;
const {
data: initialProps,
baggage,
sentryTrace,
}: {
data: {
_sentryTraceData?: string;
_sentryBaggage?: string;
};
baggage?: string;
sentryTrace?: string;
} = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction.

const activeSpan = getActiveSpan();
const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined);

if (requestSpan) {
const sentryTrace = spanToTraceHeader(requestSpan);

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
initialProps._sentryTraceData = sentryTrace;
}

const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan);
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
initialProps._sentryTraceData = sentryTrace;
}

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
initialProps._sentryBaggage = baggage;
}
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
initialProps._sentryBaggage = baggage;
}

return initialProps;
Expand Down
36 changes: 14 additions & 22 deletions packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { getActiveSpan, getDynamicSamplingContextFromSpan, getRootSpan, spanToTraceHeader } from '@sentry/core';
import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils';
import type { GetServerSideProps } from 'next';

import { isBuild } from './utils/isBuild';
import { getSpanFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';
import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils';

/**
* Create a wrapped version of the user's exported `getServerSideProps` function
Expand Down Expand Up @@ -32,27 +30,21 @@ export function wrapGetServerSidePropsWithSentry(
dataFetchingMethodName: 'getServerSideProps',
});

const serverSideProps = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType<
typeof tracedGetServerSideProps
>);
const {
data: serverSideProps,
baggage,
sentryTrace,
} = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType<typeof tracedGetServerSideProps>);

if (serverSideProps && 'props' in serverSideProps) {
const activeSpan = getActiveSpan();
const requestSpan = getSpanFromRequest(req) ?? (activeSpan ? getRootSpan(activeSpan) : undefined);
if (requestSpan) {
const sentryTrace = spanToTraceHeader(requestSpan);

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
(serverSideProps.props as Record<string, unknown>)._sentryTraceData = sentryTrace;
}

const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan);
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
(serverSideProps.props as Record<string, unknown>)._sentryBaggage = baggage;
}
// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (sentryTrace) {
(serverSideProps.props as Record<string, unknown>)._sentryTraceData = sentryTrace;
}

// The Next.js serializer throws on undefined values so we need to guard for it (#12102)
if (baggage) {
(serverSideProps.props as Record<string, unknown>)._sentryBaggage = baggage;
}
}

Expand Down
4 changes: 1 addition & 3 deletions packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function wrapGetStaticPropsWithSentry(
parameterizedRoute: string,
): GetStaticProps<Props> {
return new Proxy(origGetStaticPropsa, {
apply: (wrappingTarget, thisArg, args: Parameters<GetStaticProps<Props>>) => {
apply: async (wrappingTarget, thisArg, args: Parameters<GetStaticProps<Props>>) => {
if (isBuild()) {
return wrappingTarget.apply(thisArg, args);
}
Expand All @@ -27,8 +27,6 @@ export function wrapGetStaticPropsWithSentry(
parameterizedRoute,
dataFetchingMethodName: 'getStaticProps',
});

return errorWrappedGetStaticProps.apply(thisArg, args);
},
});
}

0 comments on commit 043ae7e

Please sign in to comment.