@sentry/remix
Version:
Official Sentry SDK for Remix
193 lines (190 loc) • 7.31 kB
JavaScript
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