@sentry/remix
Version:
Official Sentry SDK for Remix
153 lines (150 loc) • 5.89 kB
JavaScript
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { consoleSandbox } from '@sentry/core';
import { remixV3Channels } from '@sentry/server-utils/orchestrion/config';
import { getDebugId, injectDebugIdSnippet, findDebugId, addDebugIdToSourceMap } from './debugId.js';
import { getShimUrl, resolveShimPath, isOrchestrionLoader, orchestrionLoader } from './orchestrionLoader.js';
const SOURCE_MAPPING_URL_REGEX = /\n\/\/# sourceMappingURL=\S+\s*$/;
const MAX_STAMPED_BODIES = 2e3;
const SDK_PACKAGE = "@sentry/remix";
let instrumented = false;
function instrumentAssetServer() {
if (instrumented) {
return;
}
instrumented = true;
diagnosticsChannel.tracingChannel(remixV3Channels.REMIX_V3_CREATE_ASSET_SERVER).subscribe({
start(data) {
try {
const context = data;
const options = context.arguments[0];
context._sentryHideSourceMaps = options?.sourceMaps === void 0;
context.arguments[0] = withDebugIdOptions(options);
} catch {
}
},
end(data) {
try {
const { result, _sentryHideSourceMaps } = data;
if (result) {
stampServedAssets(result, { hideSourceMaps: Boolean(_sentryHideSourceMaps) });
}
} catch {
}
},
asyncStart() {
},
asyncEnd() {
},
error() {
}
});
}
const debugIdLoader = (url, context, nextLoad) => {
const result = nextLoad(url, context);
if (result.format !== "module" || typeof result.source !== "string") {
return result;
}
const debugId = getDebugId(`${context.moduleUrl ?? url}
${result.source}`);
return { ...result, source: injectDebugIdSnippet(result.source, debugId) };
};
function withDebugIdOptions(options) {
if (!options) {
return options;
}
if (options.sourceMaps === false) {
consoleSandbox(() => {
console.warn(
"[Sentry] Source maps are disabled in your asset server (`sourceMaps: false`). Sentry will not override this, so client stack traces stay minified."
);
});
}
const servesPackages = (options.allowPackages?.length ?? 0) > 0;
const allowPackages = servesPackages && !options.allowPackages?.includes(SDK_PACKAGE) ? [...options.allowPackages ?? [], SDK_PACKAGE] : options.allowPackages;
const shimUrl = servesPackages ? getShimUrl(options.basePath ?? "/", options.rootDir ?? process.cwd(), options.mounts, resolveShimPath()) : void 0;
if (servesPackages && shimUrl === void 0) {
consoleSandbox(() => {
console.warn(
"[Sentry] The browser channel shim is outside the asset server's node_modules mount, so browser modules are served without instrumentation. Component render errors need `captureRuntimeErrors(app)`."
);
});
}
const loaders = (options.scripts?.loaders ?? []).filter(
(loader) => loader !== debugIdLoader && !isOrchestrionLoader(loader)
);
const external = (options.scripts?.external ?? []).filter((entry) => entry !== shimUrl);
return {
...options,
...allowPackages !== options.allowPackages && { allowPackages },
// Hidden unless the app chose otherwise, see `stampServedAssets`.
sourceMaps: options.sourceMaps ?? "external",
scripts: {
...options.scripts,
// The transform first, the debug ID last, so the ID covers the transformed module.
loaders: shimUrl === void 0 ? [...loaders, debugIdLoader] : [...loaders, orchestrionLoader(shimUrl), debugIdLoader],
// Kept as a URL by the compiler. A bare specifier would not resolve from inside an instrumented
// package under pnpm.
external: shimUrl === void 0 ? external : [shimUrl, ...external]
}
};
}
const UNSTAMPED_FETCH = /* @__PURE__ */ Symbol.for("sentry.remix.unstampedFetch");
function stampServedAssets(server, { hideSourceMaps }) {
const fetchAsset = server.fetch;
server[UNSTAMPED_FETCH] = fetchAsset;
const stamped = /* @__PURE__ */ new Map();
server.fetch = async (request) => {
const response = await fetchAsset(request);
if (response?.status !== 200 || request.method !== "GET") {
return response;
}
const url = new URL(request.url);
const contentType = response.headers.get("content-type") ?? "";
const etag = response.headers.get("etag");
if (contentType.includes("javascript")) {
return withBody(response, await remember(etag, async () => stampModule(await response.text())));
}
if (url.pathname.endsWith(".map")) {
if (hideSourceMaps) {
return null;
}
const body = await remember(etag, async () => {
url.pathname = url.pathname.slice(0, -".map".length);
const moduleResponse = await fetchAsset(new Request(url));
const debugId = moduleResponse?.ok ? findDebugId(await moduleResponse.text()) : void 0;
const map = await response.text();
return debugId ? addDebugIdToSourceMap(map, debugId) : map;
});
return withBody(response, body);
}
return response;
};
function stampModule(served) {
const code = hideSourceMaps ? served.replace(SOURCE_MAPPING_URL_REGEX, "") : served;
const debugId = findDebugId(code);
return debugId ? `${code}
//# debugId=${debugId}` : code;
}
async function remember(etag, compute) {
if (etag === null) {
return compute();
}
const cached = stamped.get(etag);
if (cached !== void 0) {
return cached;
}
const body = await compute();
if (stamped.size >= MAX_STAMPED_BODIES) {
stamped.delete(stamped.keys().next().value);
}
stamped.set(etag, body);
return body;
}
}
function withBody(response, body) {
const headers = new Headers(response.headers);
headers.delete("content-length");
return new Response(body, { status: response.status, statusText: response.statusText, headers });
}
export { UNSTAMPED_FETCH, debugIdLoader, instrumentAssetServer };
//# sourceMappingURL=assetServer.js.map