@sentry/nextjs
Version:
Official Sentry SDK for Next.js
120 lines (109 loc) • 4.38 kB
JavaScript
import { withIsolationScope, getClient, logger, getActiveSpan, continueTrace, startSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors, SPAN_STATUS_ERROR, captureException, getIsolationScope, vercelWaitUntil } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build.js';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils.js';
import { flushSafelyWithTimeout } from './utils/responseEnd.js';
/**
* Wraps a Next.js Server Action implementation with Sentry Error and Performance instrumentation.
*/
function withServerActionInstrumentation(
...args
) {
if (typeof args[1] === 'function') {
const [serverActionName, callback] = args;
return withServerActionInstrumentationImplementation(serverActionName, {}, callback);
} else {
const [serverActionName, options, callback] = args;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return withServerActionInstrumentationImplementation(serverActionName, options, callback);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function withServerActionInstrumentationImplementation(
serverActionName,
options,
callback,
) {
return withIsolationScope(async isolationScope => {
const sendDefaultPii = getClient()?.getOptions().sendDefaultPii;
let sentryTraceHeader;
let baggageHeader;
const fullHeadersObject = {};
try {
const awaitedHeaders = await options.headers;
sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined;
baggageHeader = awaitedHeaders?.get('baggage');
awaitedHeaders?.forEach((value, key) => {
fullHeadersObject[key] = value;
});
} catch (e) {
DEBUG_BUILD &&
logger.warn(
"Sentry wasn't able to extract the tracing headers for a server action. Will not trace this request.",
);
}
isolationScope.setTransactionName(`serverAction/${serverActionName}`);
isolationScope.setSDKProcessingMetadata({
normalizedRequest: {
headers: fullHeadersObject,
} ,
});
// 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: sentryTraceHeader,
baggage: baggageHeader,
},
async () => {
try {
return await startSpan(
{
op: 'function.server_action',
name: `serverAction/${serverActionName}`,
forceTransaction: true,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
},
},
async span => {
const result = await handleCallbackErrors(callback, error => {
if (isNotFoundNavigationError(error)) {
// We don't want to report "not-found"s
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
} else if (isRedirectNavigationError(error)) {
// Don't do anything for redirects
} else {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
captureException(error, {
mechanism: {
handled: false,
},
});
}
});
if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) {
getIsolationScope().setExtra('server_action_result', result);
}
if (options.formData) {
options.formData.forEach((value, key) => {
getIsolationScope().setExtra(
`server_action_form_data.${key}`,
typeof value === 'string' ? value : '[non-string value]',
);
});
}
return result;
},
);
} finally {
vercelWaitUntil(flushSafelyWithTimeout());
}
},
);
});
}
export { withServerActionInstrumentation };
//# sourceMappingURL=withServerActionInstrumentation.js.map