Files
SnapOtter/apps/api/src/instrument.ts
T
SnapOtterandGitHub ae6a4c8b7c fix: error-only Sentry telemetry, storm-proof capture, and crash fixes (#476)
Removes Sentry tracing entirely (BullMQ idle polling burned 4.8M transactions in 2 days at the baked 0.1 rate), decouples PostHog sampling, and replaces the type-only error scrub with a vetted-field sanitizer plus SafeError/ToolInputError contracts. One classified capture path with per-signature throttles and a per-process ceiling makes storms impossible (NODE-1E was 4,541 events from one 30s loop). Browser errors move to a dedicated web Sentry project with their own source maps. Adds the SNAPOTTER_TELEMETRY runtime kill switch and silences test fleets.

Crash fixes: remote 204/304 SSRF process kill (NODE-20), conversion-preset boot crash loop (NODE-21), Redis version preflight + unhandled subscribe rejection (NODE-1T), Sign PDF on plain-http origins (NODE-1K/1M), wavesurfer/pdf.js teardown rejections (NODE-1P/1N), bundle-import ZlibError to 400 (NODE-1Z), chart-maker input errors declassified (NODE-1H/1J), asset requests skip the session DB lookup (NODE-1D).
2026-07-10 21:41:49 +08:00

55 lines
2.5 KiB
TypeScript

import { existsSync } from "node:fs";
import { ANALYTICS_BAKED } from "@snapotter/shared";
import { analyticsEnabled, gatePrimed, telemetryEnvKilled } from "./lib/analytics-gate.js";
import { buildBeforeSend } from "./lib/sentry-scrub.js";
// Sentry inits at process load, before the gate cache is primed. Until the
// first successful read, stay silent rather than emit on the default-ON cache,
// so an opted-out instance never reports even a boot-window crash.
const sentryActive = () => gatePrimed() && analyticsEnabled();
// All-in-one detection: docker/entrypoint.sh exports EMBEDDED_MODE=1 before
// exec'ing s6-overlay, and the snapotter service run script is with-contenv,
// so the marker reaches this process. URL absence is not a usable signal:
// embedded mode sets loopback DATABASE_URL/REDIS_URL before boot, and native
// dev commonly leaves DATABASE_URL unset (config.ts defaults it).
function deployMode(): string {
if (process.env.EMBEDDED_MODE) return "embedded";
if (existsSync("/.dockerenv")) return "external";
return "native";
}
if (ANALYTICS_BAKED.sentryDsn && !telemetryEnvKilled()) {
try {
const Sentry = await import("@sentry/node");
const { APP_VERSION } = await import("@snapotter/shared");
// The Docker build sets SENTRY_RELEASE to the release version so errors
// attribute to a build; falls back to APP_VERSION for non-image runs.
const release = process.env.SENTRY_RELEASE || APP_VERSION;
// buildBeforeSend is typed on loose Record shapes so sentry-scrub.ts never
// imports @sentry/node; cast at this one boundary to the SDK callback type.
type SentryOptions = NonNullable<Parameters<typeof Sentry.init>[0]>;
Sentry.init({
dsn: ANALYTICS_BAKED.sentryDsn,
release,
environment: process.env.SNAPOTTER_ENV || "production",
sendDefaultPii: false,
// Errors only. No traces options are set at all, so the SDK never
// starts traces and BullMQ/pg idle polling can't become transactions
// again (the July 2026 quota incident).
integrations: [Sentry.httpIntegration({ trackIncomingRequestsAsSessions: false })],
sendClientReports: false,
maxBreadcrumbs: 0,
beforeBreadcrumb: () => null,
initialScope: { tags: { deploy_mode: deployMode() } },
beforeSend: buildBeforeSend(sentryActive) as unknown as SentryOptions["beforeSend"],
});
console.log("[sentry] initialized (errors only), release:", release);
} catch {
// @sentry/node not available
}
}