UNPKG

@sentry/remix

Version:
362 lines (358 loc) • 14.4 kB
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