@sentry/remix
Version:
Official Sentry SDK for Remix
356 lines (353 loc) • 14 kB
JavaScript
import { isRouteErrorResponse, isDeferredData } from '@remix-run/router';
import { debug, withIsolationScope, getClient, winterCGRequestToRequestData, HTTP_SPAN_NAME_FALLBACK, hasSpanStreamingEnabled, getActiveSpan, getRootSpan, hasSpansEnabled, continueTrace, startSpan, httpHeadersToSpanAttributes, winterCGHeadersToDict, filterCollectedUrl, setHttpStatus, fill, spanToJSON, getTraceData } from '@sentry/core';
import { loadModule, isNodeEnv } from '@sentry/core/server';
import { DEBUG_BUILD } from '../utils/debug-build.js';
import { createRoutes, getTransactionName, isCloudflareEnv } from '../utils/utils.js';
import { isResponse, extractData, json } from '../utils/vendor/response.js';
import { captureRemixServerException, errorHandleDataFunction } from './errors.js';
import { generateSentryServerTimingHeader, injectServerTimingHeaderValue } from './serverTimingTracePropagation.js';
import { HTTP_ROUTE, SENTRY_SEGMENT_NAME_SOURCE, URL_PATH, URL_FULL, SENTRY_OP, SENTRY_ORIGIN, CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, ROUTER_NAVIGATION_ROUTE_ID } from '@sentry/conventions/attributes';
import { HTTP_SERVER, FUNCTION } from '@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 (isResponse(err) || isRouteErrorResponse(err)) {
return;
}
captureRemixServerException(err, "remix.server.handleError", request).then(null, (e) => {
DEBUG_BUILD && 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 (isNodeEnv() || isCloudflareEnv()) {
const traceData = getTraceData();
return {
sentryTrace: traceData["sentry-trace"],
sentryBaggage: traceData.baggage
};
}
return {};
}
function makeWrappedDocumentRequestFunction(instrumentTracing) {
return function(origDocumentRequestFunction) {
return async function(request, ...args) {
const serverTimingHeader = generateSentryServerTimingHeader();
let response;
if (instrumentTracing) {
const activeSpan = getActiveSpan();
const rootSpan = activeSpan && getRootSpan(activeSpan);
const client = getClient();
const description = (rootSpan ? spanToJSON(rootSpan).name : void 0) || "<unknown>";
const name = client && hasSpanStreamingEnabled(client) ? "documentRequest" : description;
response = await startSpan(
{
name,
onlyIfParent: true,
attributes: {
method: request.method,
[URL_FULL]: filterCollectedUrl(request.url),
[SENTRY_ORIGIN]: "auto.function.remix",
[SENTRY_OP]: FUNCTION,
[SENTRY_DESCRIPTION]: description,
[CODE_FUNCTION_NAME]: "documentRequest"
}
},
() => {
return origDocumentRequestFunction.call(this, request, ...args);
}
);
} else {
response = await origDocumentRequestFunction.call(this, request, ...args);
}
if (serverTimingHeader && response instanceof Response) {
return injectServerTimingHeaderValue(response, serverTimingHeader);
}
return response;
};
};
}
function updateSpanWithRoute(args, build) {
try {
const activeSpan = getActiveSpan();
const rootSpan = activeSpan && getRootSpan(activeSpan);
if (!rootSpan) {
return;
}
const routes = createRoutes(build.routes);
const url = new URL(args.request.url);
const [transactionName, source] = getTransactionName(routes, url);
const method = args.request.method.toUpperCase();
const currentSpanName = spanToJSON(rootSpan).name;
const newSpanName = currentSpanName?.startsWith(method) ? `${method} ${transactionName}` : transactionName;
const client = getClient();
const isUnparameterizedStreamedSpan = source !== "route" && !!client && hasSpanStreamingEnabled(client);
if (!isUnparameterizedStreamedSpan) {
rootSpan.updateName(newSpanName);
}
rootSpan.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, source);
if (source === "route") {
rootSpan.setAttribute(HTTP_ROUTE, transactionName);
}
} catch (e) {
DEBUG_BUILD && 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 = getClient();
res = await 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 && hasSpanStreamingEnabled(client) ? name : id,
attributes: {
[SENTRY_ORIGIN]: "auto.ui.remix",
[SENTRY_OP]: FUNCTION,
[SENTRY_DESCRIPTION]: id,
[CODE_FUNCTION_NAME]: name,
[ROUTER_NAVIGATION_ROUTE_ID]: id
}
},
(span) => {
return errorHandleDataFunction.call(this, origFn, name, args, span);
}
);
} else {
res = await errorHandleDataFunction.call(this, origFn, name, args);
}
if (isResponse(res) && isRedirectResponse(res)) {
const serverTimingHeader = generateSentryServerTimingHeader();
if (serverTimingHeader) {
return 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 (isDeferredData(res)) {
res.data["sentryTrace"] = traceAndBaggage.sentryTrace;
res.data["sentryBaggage"] = traceAndBaggage.sentryBaggage;
return res;
}
if (isResponse(res)) {
if (isRedirectResponse(res) || isCatchResponse(res)) {
DEBUG_BUILD && debug.warn("Skipping injection of trace and baggage as the response does not have a body");
return res;
} else {
const data = await extractData(res);
if (typeof data === "object") {
return json(
{ ...data, ...traceAndBaggage },
{
headers: res.headers,
statusText: res.statusText,
status: res.status
}
);
} else {
DEBUG_BUILD && 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 = createRoutes(resolvedBuild.build.routes);
} else {
resolvedRoutes = createRoutes(resolvedBuild.routes);
}
}
return withIsolationScope(async (isolationScope) => {
const client = getClient();
let normalizedRequest = {};
try {
normalizedRequest = winterCGRequestToRequestData(request);
} catch {
DEBUG_BUILD && debug.warn("Failed to normalize Remix request");
}
const url = new URL(request.url);
if (options?.instrumentTracing && resolvedRoutes) {
[name, source] = getTransactionName(resolvedRoutes, url);
isolationScope.setTransactionName(name);
spanName = source === "route" || !client || !hasSpanStreamingEnabled(client) ? name : request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK;
const parentSpan = getActiveSpan();
if (parentSpan) {
const rootSpan = getRootSpan(parentSpan);
rootSpan?.updateName(spanName);
rootSpan?.setAttributes({
[SENTRY_SEGMENT_NAME_SOURCE]: source,
...source === "route" && {
[HTTP_ROUTE]: name
}
});
}
}
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
if (!client || !hasSpansEnabled(client.getOptions())) {
return origRequestHandler.call(this, request, loadContext);
}
return continueTrace(
{
sentryTrace: request.headers.get("sentry-trace") || "",
baggage: request.headers.get("baggage") || ""
},
async () => {
if (options?.instrumentTracing) {
const parentSpan = getActiveSpan();
const rootSpan = parentSpan && getRootSpan(parentSpan);
rootSpan?.updateName(spanName);
rootSpan?.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, source);
return startSpan(
{
name: spanName,
attributes: {
[SENTRY_ORIGIN]: "auto.http.remix",
[SENTRY_SEGMENT_NAME_SOURCE]: source,
[SENTRY_OP]: HTTP_SERVER,
[URL_FULL]: filterCollectedUrl(url.href),
[URL_PATH]: url.pathname,
method: request.method,
...source === "route" && {
[HTTP_ROUTE]: name
},
...httpHeadersToSpanAttributes(
winterCGHeadersToDict(request.headers),
client.getDataCollectionOptions()
)
}
},
async (span) => {
const res = await origRequestHandler.call(this, request, loadContext);
if (isResponse(res)) {
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__) {
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__) {
fill(wrappedRoute.module, "loader", makeWrappedRootLoader(options?.instrumentTracing, build));
}
}
const routeAction = wrappedRoute.module.action;
if (routeAction && !routeAction.__sentry_original__) {
fill(wrappedRoute.module, "action", makeWrappedAction(id, options?.instrumentTracing, build));
}
const routeLoader = wrappedRoute.module.loader;
if (routeLoader && !routeLoader.__sentry_original__) {
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 = loadModule("@remix-run/server-runtime", module);
if (!pkg) {
DEBUG_BUILD && debug.warn("Remix SDK was unable to require `@remix-run/server-runtime` package.");
return;
}
if (pkg.createRequestHandler.__sentry_original__) {
return;
}
fill(pkg, "createRequestHandler", makeWrappedCreateRequestHandler(options));
}
export { instrumentBuild, instrumentServer, makeWrappedCreateRequestHandler, sentryHandleError, wrapHandleErrorWithSentry };
//# sourceMappingURL=instrumentServer.js.map