@sentry/nextjs
Version:
Official Sentry SDK for Next.js
222 lines (198 loc) • 8.94 kB
JavaScript
import { _nullishCoalesce, _optionalChain } from '@sentry/utils';
import { runWithAsyncContext, getCurrentScope, startTransaction, getActiveTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, getActiveSpan } from '@sentry/core';
import { isString, tracingContextFromHeaders } from '@sentry/utils';
import { platformSupportsStreaming } from './platformSupportsStreaming.js';
import { autoEndTransactionOnResponseEnd, flushQueue } from './responseEnd.js';
/**
* 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.
*/
function getTransactionFromRequest(req) {
return req._sentryTransaction;
}
function setTransactionOnRequest(transaction, req) {
req._sentryTransaction = transaction;
}
/**
* 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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function withErrorInstrumentation(
origFunction,
) {
return async function ( ...origFunctionArguments) {
try {
return await origFunction.apply(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, { mechanism: { handled: false } });
throw e;
}
};
}
/**
* Calls a server-side data fetching function (that takes a `req` and `res` object in its context) with tracing
* instrumentation. A transaction will be created for the incoming request (if it doesn't already exist) in addition to
* a span for the wrapped data fetching function.
*
* All of the above happens in an isolated domain, meaning all thrown errors will be associated with the correct span.
*
* @param origDataFetcher The data fetching method to call.
* @param origFunctionArguments The arguments to call the data fetching method with.
* @param req The data fetching function's request object.
* @param res The data fetching function's response object.
* @param options Options providing details for the created transaction and span.
* @returns what the data fetching method call returned.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function withTracedServerSideDataFetcher(
origDataFetcher,
req,
res,
options
,
) {
return async function ( ...args) {
return runWithAsyncContext(async () => {
const scope = getCurrentScope();
const previousSpan = _nullishCoalesce(getTransactionFromRequest(req), () => ( getActiveSpan()));
let dataFetcherSpan;
const sentryTrace =
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined;
const baggage = _optionalChain([req, 'access', _ => _.headers, 'optionalAccess', _2 => _2.baggage]);
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
sentryTrace,
baggage,
);
scope.setPropagationContext(propagationContext);
if (platformSupportsStreaming()) {
let spanToContinue;
if (previousSpan === undefined) {
// TODO: Refactor this to use `startSpan()`
// eslint-disable-next-line deprecation/deprecation
const newTransaction = startTransaction(
{
op: 'http.server',
name: options.requestedRouteName,
origin: 'auto.function.nextjs',
...traceparentData,
status: 'ok',
metadata: {
request: req,
source: 'route',
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
},
{ request: req },
);
if (platformSupportsStreaming()) {
// On platforms that don't support streaming, doing things after res.end() is unreliable.
autoEndTransactionOnResponseEnd(newTransaction, res);
}
// Link the transaction and the request together, so that when we would normally only have access to one, it's
// still possible to grab the other.
setTransactionOnRequest(newTransaction, req);
spanToContinue = newTransaction;
} else {
spanToContinue = previousSpan;
}
// eslint-disable-next-line deprecation/deprecation
dataFetcherSpan = spanToContinue.startChild({
op: 'function.nextjs',
description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
origin: 'auto.function.nextjs',
status: 'ok',
});
} else {
// TODO: Refactor this to use `startSpan()`
// eslint-disable-next-line deprecation/deprecation
dataFetcherSpan = startTransaction({
op: 'function.nextjs',
name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`,
origin: 'auto.function.nextjs',
...traceparentData,
status: 'ok',
metadata: {
request: req,
source: 'route',
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
});
}
// eslint-disable-next-line deprecation/deprecation
scope.setSpan(dataFetcherSpan);
scope.setSDKProcessingMetadata({ request: req });
try {
return await origDataFetcher.apply(this, args);
} catch (e) {
// Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation`
// that set the transaction status, we need to manually set the status of the span & transaction
dataFetcherSpan.setStatus('internal_error');
_optionalChain([previousSpan, 'optionalAccess', _3 => _3.setStatus, 'call', _4 => _4('internal_error')]);
throw e;
} finally {
dataFetcherSpan.end();
// eslint-disable-next-line deprecation/deprecation
scope.setSpan(previousSpan);
if (!platformSupportsStreaming()) {
await flushQueue();
}
}
});
};
}
/**
* Call a data fetcher and trace it. Only traces the function if there is an active transaction on the scope.
*
* We only do the following until we move transaction creation into this function: When called, the wrapped function
* will also update the name of the active transaction with a parameterized route provided via the `options` argument.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function callDataFetcherTraced(
origFunction,
origFunctionArgs,
options
,
) {
const { parameterizedRoute, dataFetchingMethodName } = options;
// eslint-disable-next-line deprecation/deprecation
const transaction = getActiveTransaction();
if (!transaction) {
return origFunction(...origFunctionArgs);
}
// TODO: Make sure that the given route matches the name of the active transaction (to prevent background data
// fetching from switching the name to a completely other route) -- We'll probably switch to creating a transaction
// right here so making that check will probabably not even be necessary.
// Logic will be: If there is no active transaction, start one with correct name and source. If there is an active
// transaction, create a child span with correct name and source.
transaction.updateName(parameterizedRoute);
transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
// Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another
// route's transaction
// eslint-disable-next-line deprecation/deprecation
const span = transaction.startChild({
op: 'function.nextjs',
origin: 'auto.function.nextjs',
description: `${dataFetchingMethodName} (${parameterizedRoute})`,
status: 'ok',
});
try {
return await origFunction(...origFunctionArgs);
} catch (err) {
// Since we finish the span before the error can bubble up and trigger the handlers in `registerErrorInstrumentation`
// that set the transaction status, we need to manually set the status of the span & transaction
transaction.setStatus('internal_error');
span.setStatus('internal_error');
span.end();
// TODO Copy more robust error handling over from `withSentry`
captureException(err, { mechanism: { handled: false } });
throw err;
}
}
export { callDataFetcherTraced, getTransactionFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher };
//# sourceMappingURL=wrapperUtils.js.map