Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
126 lines
5.4 KiB
Plaintext
126 lines
5.4 KiB
Plaintext
import { debug, withIsolationScope, getActiveSpan, continueTrace, httpRequestToRequestData, startSpanManual, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus, objectify, captureException, isString } from '@sentry/core';
|
|
import { waitUntil, flushSafelyWithTimeout } from '../utils/responseEnd.js';
|
|
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils.js';
|
|
|
|
/**
|
|
* Wrap the given API route handler with error nad performance monitoring.
|
|
*
|
|
* @param apiHandler The handler exported from the user's API page route file, which may or may not already be
|
|
* wrapped with `withSentry`
|
|
* @param parameterizedRoute The page's parameterized route.
|
|
* @returns The wrapped handler which will always return a Promise.
|
|
*/
|
|
function wrapApiHandlerWithSentry(apiHandler, parameterizedRoute) {
|
|
return new Proxy(apiHandler, {
|
|
apply: (
|
|
wrappingTarget,
|
|
thisArg,
|
|
args,
|
|
) => {
|
|
dropNextjsRootContext();
|
|
return escapeNextjsTracing(() => {
|
|
const [req, res] = args;
|
|
|
|
if (!req) {
|
|
debug.log(
|
|
`Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`,
|
|
);
|
|
return wrappingTarget.apply(thisArg, args);
|
|
} else if (!res) {
|
|
debug.log(
|
|
`Wrapped API handler on route "${parameterizedRoute}" was not passed a response object. Will not instrument.`,
|
|
);
|
|
return wrappingTarget.apply(thisArg, args);
|
|
}
|
|
|
|
// Prevent double wrapping of the same request.
|
|
if (req.__withSentry_applied__) {
|
|
return wrappingTarget.apply(thisArg, args);
|
|
}
|
|
req.__withSentry_applied__ = true;
|
|
|
|
return withIsolationScope(isolationScope => {
|
|
// Normally, there is an active span here (from Next.js OTEL) and we just use that as parent
|
|
// Else, we manually continueTrace from the incoming headers
|
|
const continueTraceIfNoActiveSpan = getActiveSpan()
|
|
? (_opts, callback) => callback()
|
|
: continueTrace;
|
|
|
|
return continueTraceIfNoActiveSpan(
|
|
{
|
|
sentryTrace:
|
|
req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
|
|
baggage: req.headers?.baggage,
|
|
},
|
|
() => {
|
|
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
|
|
const normalizedRequest = httpRequestToRequestData(req);
|
|
|
|
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
|
|
isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`);
|
|
|
|
return startSpanManual(
|
|
{
|
|
name: `${reqMethod}${parameterizedRoute}`,
|
|
op: 'http.server',
|
|
forceTransaction: true,
|
|
attributes: {
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
|
|
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
|
|
},
|
|
},
|
|
async span => {
|
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
res.end = new Proxy(res.end, {
|
|
apply(target, thisArg, argArray) {
|
|
setHttpStatus(span, res.statusCode);
|
|
span.end();
|
|
waitUntil(flushSafelyWithTimeout());
|
|
return target.apply(thisArg, argArray);
|
|
},
|
|
});
|
|
try {
|
|
return await wrappingTarget.apply(thisArg, args);
|
|
} catch (e) {
|
|
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
|
|
// store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
|
|
// to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a
|
|
// way to prevent it from actually being reported twice.)
|
|
const objectifiedErr = objectify(e);
|
|
|
|
captureException(objectifiedErr, {
|
|
mechanism: {
|
|
type: 'auto.http.nextjs.api_handler',
|
|
handled: false,
|
|
data: {
|
|
wrapped_handler: wrappingTarget.name,
|
|
function: 'withSentry',
|
|
},
|
|
},
|
|
});
|
|
|
|
setHttpStatus(span, 500);
|
|
span.end();
|
|
|
|
// we need to await the flush here to ensure that the error is captured
|
|
// as the runtime freezes as soon as the error is thrown below
|
|
await flushSafelyWithTimeout();
|
|
|
|
// We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
|
|
// would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
|
|
// the error as already having been captured.)
|
|
throw objectifiedErr;
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export { wrapApiHandlerWithSentry };
|
|
//# sourceMappingURL=wrapApiHandlerWithSentry.js.map
|