UNPKG

@sentry/nextjs

Version:
231 lines (205 loc) • 9.11 kB
var { _nullishCoalesce, _optionalChain } = require('@sentry/utils'); Object.defineProperty(exports, '__esModule', { value: true }); const core = require('@sentry/core'); const utils = require('@sentry/utils'); const platformSupportsStreaming = require('./platformSupportsStreaming.js'); const responseEnd = require('./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. core.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 core.runWithAsyncContext(async () => { const scope = core.getCurrentScope(); const previousSpan = _nullishCoalesce(getTransactionFromRequest(req), () => ( core.getActiveSpan())); let dataFetcherSpan; const sentryTrace = req.headers && utils.isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; const baggage = _optionalChain([req, 'access', _ => _.headers, 'optionalAccess', _2 => _2.baggage]); const { traceparentData, dynamicSamplingContext, propagationContext } = utils.tracingContextFromHeaders( sentryTrace, baggage, ); scope.setPropagationContext(propagationContext); if (platformSupportsStreaming.platformSupportsStreaming()) { let spanToContinue; if (previousSpan === undefined) { // TODO: Refactor this to use `startSpan()` // eslint-disable-next-line deprecation/deprecation const newTransaction = core.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.platformSupportsStreaming()) { // On platforms that don't support streaming, doing things after res.end() is unreliable. responseEnd.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 = core.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.platformSupportsStreaming()) { await responseEnd.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 = core.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(core.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` core.captureException(err, { mechanism: { handled: false } }); throw err; } } exports.callDataFetcherTraced = callDataFetcherTraced; exports.getTransactionFromRequest = getTransactionFromRequest; exports.withErrorInstrumentation = withErrorInstrumentation; exports.withTracedServerSideDataFetcher = withTracedServerSideDataFetcher; //# sourceMappingURL=wrapperUtils.js.map