UNPKG

@sentry/remix

Version:
193 lines (190 loc) • 7.31 kB
import * as diagnosticsChannel from 'node:diagnostics_channel'; import { waitForTracingChannelBinding, getClient, hasSpanStreamingEnabled, startInactiveSpan, HTTP_SPAN_NAME_FALLBACK, isObjectLike, parseStringToURLObject, filterCollectedUrl, isURLObjectRelative, getSpanStatusFromHttpCode, getActiveSpan, spanToJSON } from '@sentry/core'; import { bindTracingChannelToSpan } from '@sentry/server-utils'; import { HTTP_REQUEST_METHOD, URL_PATH, CODE_FUNCTION_NAME, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN, SENTRY_KIND, SENTRY_DESCRIPTION, URL_FULL, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ROUTER_NAVIGATION_ROUTE_ID } from '@sentry/conventions/attributes'; import { HTTP_SERVER, FUNCTION } from '@sentry/conventions/op'; import { remixChannels } from '@sentry/server-utils/orchestrion/config'; import { applyFormDataAttributes } from '../../utils/formData.js'; const ORIGIN = "auto.http.remix"; const NOOP = () => { }; const MATCH_ROUTE_ID = "match.route.id"; const MATCH_PARAMS = "match.params"; function getRequestAttributes(request) { if (!isObjectLike(request)) { return {}; } const { method, url } = request; const attributes = {}; if (typeof method === "string") { attributes[HTTP_REQUEST_METHOD] = method; } if (typeof url === "string") { const urlObject = parseStringToURLObject(url); attributes[URL_FULL] = filterCollectedUrl( urlObject && !isURLObjectRelative(urlObject) ? urlObject.href : void 0 ); attributes[URL_PATH] = urlObject?.pathname; } return attributes; } function getMatchAttributes(params) { const attributes = {}; if (params.routeId) { attributes[MATCH_ROUTE_ID] = params.routeId; attributes[ROUTER_NAVIGATION_ROUTE_ID] = params.routeId; } for (const [name, value] of Object.entries(params.params ?? {})) { attributes[`${MATCH_PARAMS}.${name}`] = value || "(undefined)"; } return attributes; } function setResponseStatus(span, result) { if (!isObjectLike(result)) { return; } const status = result.status; if (typeof status === "number") { span.setAttribute(HTTP_RESPONSE_STATUS_CODE, status); const spanStatus = getSpanStatusFromHttpCode(status); span.setStatus(spanStatus); } } function enrichActiveSpanWithRoute(result) { const span = getActiveSpan(); if (!span) { return; } const matches = Array.isArray(result) ? result : []; const route = matches[matches.length - 1]?.route; if (route?.path) { span.setAttribute(HTTP_ROUTE, route.path); const method = spanToJSON(span).attributes[HTTP_REQUEST_METHOD]; span.updateName(typeof method === "string" ? `${method} ${route.path}` : route.path); span.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, "route"); } if (route?.id) { span.setAttribute(MATCH_ROUTE_ID, route.id); } } function subscribeRequestHandler() { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_REQUEST_HANDLER), (data) => { const requestAttributes = getRequestAttributes(data.arguments[0]); const method = requestAttributes[HTTP_REQUEST_METHOD]; const path = requestAttributes[URL_PATH]; const hasUrlName = typeof method === "string" && typeof path === "string"; const client = getClient(); const isStreamed = !!client && hasSpanStreamingEnabled(client); return startInactiveSpan({ name: isStreamed ? typeof method === "string" ? method : HTTP_SPAN_NAME_FALLBACK : hasUrlName ? `${method} ${path}` : "remix.request", attributes: { [SENTRY_KIND]: "server", [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: HTTP_SERVER, ...hasUrlName && { [SENTRY_SEGMENT_NAME_SOURCE]: "url" }, [CODE_FUNCTION_NAME]: "requestHandler", ...requestAttributes } }); }, { beforeSpanEnd: (span, data) => setResponseStatus(span, data.result) } ); } function subscribeMatchServerRoutes() { diagnosticsChannel.tracingChannel(remixChannels.REMIX_MATCH_SERVER_ROUTES).subscribe({ start: NOOP, end(data) { enrichActiveSpanWithRoute(data.result); }, asyncStart: NOOP, asyncEnd: NOOP, error: NOOP }); } function subscribeCallRouteLoader() { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER), (data) => { const params = data.arguments[0] ?? {}; const client = getClient(); const description = `LOADER ${params.routeId}`; return startInactiveSpan({ // With span streaming, a `function` span is named after the function it wraps. The route id // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? "loader" : description, attributes: { [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: "loader", ...getRequestAttributes(params.request), ...getMatchAttributes(params) } }); }, { requiresParentSpan: true, beforeSpanEnd: (span, data) => setResponseStatus(span, data.result) } ); } function subscribeCallRouteAction(formDataCapture) { bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_ACTION), (data) => { const params = data.arguments[0] ?? {}; if (formDataCapture && params.request) { const formData = params.request.clone().formData(); formData.catch(() => void 0); data._sentryFormData = formData; } const client = getClient(); const description = `ACTION ${params.routeId}`; return startInactiveSpan({ // With span streaming, a `function` span is named after the function it wraps. The route id // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? "action" : description, attributes: { [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: "action", ...getRequestAttributes(params.request), ...getMatchAttributes(params) } }); }, { requiresParentSpan: true, beforeSpanEnd: (span, data) => setResponseStatus(span, data.result), // Hold the span end until the (already in-flight) form-data read resolves, then apply the // attributes and end (which sets the response status via `beforeSpanEnd`). On error, or when // capture isn't configured, let the helper end the span normally. deferSpanEnd: ({ span, data, end }) => { const formData = data._sentryFormData; if (!formDataCapture || !formData || "error" in data) { return false; } formData.then((resolved) => applyFormDataAttributes(span, resolved, formDataCapture)).catch(() => void 0).finally(() => end()); return true; } } ); } function instrumentRemix(formDataCapture) { if (!diagnosticsChannel.tracingChannel) { return; } waitForTracingChannelBinding(() => { subscribeRequestHandler(); subscribeMatchServerRoutes(); subscribeCallRouteLoader(); subscribeCallRouteAction(formDataCapture); }); } export { instrumentRemix }; //# sourceMappingURL=tracing-channel.js.map