UNPKG

@sentry/remix

Version:
356 lines (353 loc) • 14 kB
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