@sentry/remix
Version:
Official Sentry SDK for Remix
362 lines (358 loc) • 14.4 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const router = require('@remix-run/router');
const core = require('@sentry/core');
const server = require('@sentry/core/server');
const debugBuild = require('../utils/debug-build.js');
const utils = require('../utils/utils.js');
const response = require('../utils/vendor/response.js');
const errors = require('./errors.js');
const serverTimingTracePropagation = require('./serverTimingTracePropagation.js');
const attributes = require('@sentry/conventions/attributes');
const op = require('@sentry/conventions/op');
const redirectStatusCodes = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
function isRedirectResponse(response) {
return redirectStatusCodes.has(response.status);
}
function isCatchResponse(response) {
return response.headers.get("X-Remix-Catch") != null;
}
function sentryHandleError(err, { request }) {
if (response.isResponse(err) || router.isRouteErrorResponse(err)) {
return;
}
errors.captureRemixServerException(err, "remix.server.handleError", request).then(null, (e) => {
debugBuild.DEBUG_BUILD && core.debug.warn("Failed to capture Remix Server exception.", e);
});
}
function wrapHandleErrorWithSentry(origHandleError) {
return function(err, args) {
const res = origHandleError.call(this, err, args);
sentryHandleError(err, args);
return res;
};
}
function getTraceAndBaggage() {
if (server.isNodeEnv() || utils.isCloudflareEnv()) {
const traceData = core.getTraceData();
return {
sentryTrace: traceData["sentry-trace"],
sentryBaggage: traceData.baggage
};
}
return {};
}
function makeWrappedDocumentRequestFunction(instrumentTracing) {
return function(origDocumentRequestFunction) {
return async function(request, ...args) {
const serverTimingHeader = serverTimingTracePropagation.generateSentryServerTimingHeader();
let response;
if (instrumentTracing) {
const activeSpan = core.getActiveSpan();
const rootSpan = activeSpan && core.getRootSpan(activeSpan);
const client = core.getClient();
const description = (rootSpan ? core.spanToJSON(rootSpan).name : void 0) || "<unknown>";
const name = client && core.hasSpanStreamingEnabled(client) ? "documentRequest" : description;
response = await core.startSpan(
{
name,
onlyIfParent: true,
attributes: {
method: request.method,
[attributes.URL_FULL]: core.filterCollectedUrl(request.url),
[attributes.SENTRY_ORIGIN]: "auto.function.remix",
[attributes.SENTRY_OP]: op.FUNCTION,
[attributes.SENTRY_DESCRIPTION]: description,
[attributes.CODE_FUNCTION_NAME]: "documentRequest"
}
},
() => {
return origDocumentRequestFunction.call(this, request, ...args);
}
);
} else {
response = await origDocumentRequestFunction.call(this, request, ...args);
}
if (serverTimingHeader && response instanceof Response) {
return serverTimingTracePropagation.injectServerTimingHeaderValue(response, serverTimingHeader);
}
return response;
};
};
}
function updateSpanWithRoute(args, build) {
try {
const activeSpan = core.getActiveSpan();
const rootSpan = activeSpan && core.getRootSpan(activeSpan);
if (!rootSpan) {
return;
}
const routes = utils.createRoutes(build.routes);
const url = new URL(args.request.url);
const [transactionName, source] = utils.getTransactionName(routes, url);
const method = args.request.method.toUpperCase();
const currentSpanName = core.spanToJSON(rootSpan).name;
const newSpanName = currentSpanName?.startsWith(method) ? `${method} ${transactionName}` : transactionName;
const client = core.getClient();
const isUnparameterizedStreamedSpan = source !== "route" && !!client && core.hasSpanStreamingEnabled(client);
if (!isUnparameterizedStreamedSpan) {
rootSpan.updateName(newSpanName);
}
rootSpan.setAttribute(attributes.SENTRY_SEGMENT_NAME_SOURCE, source);
if (source === "route") {
rootSpan.setAttribute(attributes.HTTP_ROUTE, transactionName);
}
} catch (e) {
debugBuild.DEBUG_BUILD && core.debug.warn("Failed to update span name with route", e);
}
}
function makeWrappedDataFunction(origFn, id, name, instrumentTracing, build) {
return async function(args) {
let res;
if (instrumentTracing) {
if (build) {
updateSpanWithRoute(args, build);
}
const client = core.getClient();
res = await core.startSpan(
{
// With span streaming, a `function` span is named after the function it wraps. The route
// module id stays on `router.navigation.route.id`.
name: client && core.hasSpanStreamingEnabled(client) ? name : id,
attributes: {
[attributes.SENTRY_ORIGIN]: "auto.ui.remix",
[attributes.SENTRY_OP]: op.FUNCTION,
[attributes.SENTRY_DESCRIPTION]: id,
[attributes.CODE_FUNCTION_NAME]: name,
[attributes.ROUTER_NAVIGATION_ROUTE_ID]: id
}
},
(span) => {
return errors.errorHandleDataFunction.call(this, origFn, name, args, span);
}
);
} else {
res = await errors.errorHandleDataFunction.call(this, origFn, name, args);
}
if (response.isResponse(res) && isRedirectResponse(res)) {
const serverTimingHeader = serverTimingTracePropagation.generateSentryServerTimingHeader();
if (serverTimingHeader) {
return serverTimingTracePropagation.injectServerTimingHeaderValue(res, serverTimingHeader);
}
}
return res;
};
}
const makeWrappedAction = (id, instrumentTracing, build) => (origAction) => {
return makeWrappedDataFunction(origAction, id, "action", instrumentTracing, build);
};
const makeWrappedLoader = (id, instrumentTracing, build) => (origLoader) => {
return makeWrappedDataFunction(origLoader, id, "loader", instrumentTracing, build);
};
function makeWrappedRootLoader(instrumentTracing, build) {
return function(origLoader) {
return async function(args) {
if (instrumentTracing && build) {
updateSpanWithRoute(args, build);
}
const res = await origLoader.call(this, args);
const traceAndBaggage = getTraceAndBaggage();
if (router.isDeferredData(res)) {
res.data["sentryTrace"] = traceAndBaggage.sentryTrace;
res.data["sentryBaggage"] = traceAndBaggage.sentryBaggage;
return res;
}
if (response.isResponse(res)) {
if (isRedirectResponse(res) || isCatchResponse(res)) {
debugBuild.DEBUG_BUILD && core.debug.warn("Skipping injection of trace and baggage as the response does not have a body");
return res;
} else {
const data = await response.extractData(res);
if (typeof data === "object") {
return response.json(
{ ...data, ...traceAndBaggage },
{
headers: res.headers,
statusText: res.statusText,
status: res.status
}
);
} else {
debugBuild.DEBUG_BUILD && core.debug.warn("Skipping injection of trace and baggage as the response body is not an object");
return res;
}
}
}
return { ...res, ...traceAndBaggage };
};
};
}
function wrapRequestHandler(origRequestHandler, build, options) {
let resolvedBuild;
let name;
let spanName;
let source;
return async function(request, loadContext) {
const upperCaseMethod = request.method.toUpperCase();
if (upperCaseMethod === "OPTIONS" || upperCaseMethod === "HEAD") {
return origRequestHandler.call(this, request, loadContext);
}
let resolvedRoutes;
if (options?.instrumentTracing) {
if (typeof build === "function") {
resolvedBuild = await build();
} else {
resolvedBuild = build;
}
if ("build" in resolvedBuild) {
resolvedRoutes = utils.createRoutes(resolvedBuild.build.routes);
} else {
resolvedRoutes = utils.createRoutes(resolvedBuild.routes);
}
}
return core.withIsolationScope(async (isolationScope) => {
const client = core.getClient();
let normalizedRequest = {};
try {
normalizedRequest = core.winterCGRequestToRequestData(request);
} catch {
debugBuild.DEBUG_BUILD && core.debug.warn("Failed to normalize Remix request");
}
const url = new URL(request.url);
if (options?.instrumentTracing && resolvedRoutes) {
[name, source] = utils.getTransactionName(resolvedRoutes, url);
isolationScope.setTransactionName(name);
spanName = source === "route" || !client || !core.hasSpanStreamingEnabled(client) ? name : request.method?.toUpperCase() || core.HTTP_SPAN_NAME_FALLBACK;
const parentSpan = core.getActiveSpan();
if (parentSpan) {
const rootSpan = core.getRootSpan(parentSpan);
rootSpan?.updateName(spanName);
rootSpan?.setAttributes({
[attributes.SENTRY_SEGMENT_NAME_SOURCE]: source,
...source === "route" && {
[attributes.HTTP_ROUTE]: name
}
});
}
}
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
if (!client || !core.hasSpansEnabled(client.getOptions())) {
return origRequestHandler.call(this, request, loadContext);
}
return core.continueTrace(
{
sentryTrace: request.headers.get("sentry-trace") || "",
baggage: request.headers.get("baggage") || ""
},
async () => {
if (options?.instrumentTracing) {
const parentSpan = core.getActiveSpan();
const rootSpan = parentSpan && core.getRootSpan(parentSpan);
rootSpan?.updateName(spanName);
rootSpan?.setAttribute(attributes.SENTRY_SEGMENT_NAME_SOURCE, source);
return core.startSpan(
{
name: spanName,
attributes: {
[attributes.SENTRY_ORIGIN]: "auto.http.remix",
[attributes.SENTRY_SEGMENT_NAME_SOURCE]: source,
[attributes.SENTRY_OP]: op.HTTP_SERVER,
[attributes.URL_FULL]: core.filterCollectedUrl(url.href),
[attributes.URL_PATH]: url.pathname,
method: request.method,
...source === "route" && {
[attributes.HTTP_ROUTE]: name
},
...core.httpHeadersToSpanAttributes(
core.winterCGHeadersToDict(request.headers),
client.getDataCollectionOptions()
)
}
},
async (span) => {
const res = await origRequestHandler.call(this, request, loadContext);
if (response.isResponse(res)) {
core.setHttpStatus(span, res.status);
}
return res;
}
);
}
return await origRequestHandler.call(this, request, loadContext);
}
);
});
};
}
function instrumentBuildCallback(build, options) {
const routes = build.routes;
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
const defaultExport = wrappedEntry.module.default;
if (defaultExport && !defaultExport.__sentry_original__) {
core.fill(wrappedEntry.module, "default", makeWrappedDocumentRequestFunction(options?.instrumentTracing));
}
for (const [id, route] of Object.entries(build.routes)) {
const wrappedRoute = { ...route, module: { ...route.module } };
if (!wrappedRoute.parentId) {
if (!wrappedRoute.module.loader) {
wrappedRoute.module.loader = () => ({});
}
if (!wrappedRoute.module.loader.__sentry_original__) {
core.fill(wrappedRoute.module, "loader", makeWrappedRootLoader(options?.instrumentTracing, build));
}
}
const routeAction = wrappedRoute.module.action;
if (routeAction && !routeAction.__sentry_original__) {
core.fill(wrappedRoute.module, "action", makeWrappedAction(id, options?.instrumentTracing, build));
}
const routeLoader = wrappedRoute.module.loader;
if (routeLoader && !routeLoader.__sentry_original__) {
core.fill(wrappedRoute.module, "loader", makeWrappedLoader(id, options?.instrumentTracing, build));
}
routes[id] = wrappedRoute;
}
const instrumentedBuild = { ...build, routes };
if (wrappedEntry) {
instrumentedBuild.entry = wrappedEntry;
}
return instrumentedBuild;
}
function instrumentBuild(build, options) {
if (typeof build === "function") {
return function() {
const resolvedBuild = build();
if (resolvedBuild instanceof Promise) {
return resolvedBuild.then((build2) => {
return instrumentBuildCallback(build2, options);
});
} else {
return instrumentBuildCallback(resolvedBuild, options);
}
};
} else {
return instrumentBuildCallback(build, options);
}
}
const makeWrappedCreateRequestHandler = (options) => function(origCreateRequestHandler) {
return function(build, ...args) {
const newBuild = instrumentBuild(build, options);
const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args);
return wrapRequestHandler(requestHandler, newBuild, options);
};
};
function instrumentServer(options) {
const pkg = server.loadModule("@remix-run/server-runtime", module);
if (!pkg) {
debugBuild.DEBUG_BUILD && core.debug.warn("Remix SDK was unable to require `@remix-run/server-runtime` package.");
return;
}
if (pkg.createRequestHandler.__sentry_original__) {
return;
}
core.fill(pkg, "createRequestHandler", makeWrappedCreateRequestHandler(options));
}
exports.instrumentBuild = instrumentBuild;
exports.instrumentServer = instrumentServer;
exports.makeWrappedCreateRequestHandler = makeWrappedCreateRequestHandler;
exports.sentryHandleError = sentryHandleError;
exports.wrapHandleErrorWithSentry = wrapHandleErrorWithSentry;
//# sourceMappingURL=instrumentServer.js.map