2026-07-10 21:41:49 +08:00
|
|
|
import { existsSync } from "node:fs";
|
2026-06-24 11:05:39 +08:00
|
|
|
import { ANALYTICS_BAKED } from "@snapotter/shared";
|
2026-07-10 21:41:49 +08:00
|
|
|
import { analyticsEnabled, gatePrimed, telemetryEnvKilled } from "./lib/analytics-gate.js";
|
|
|
|
|
import { buildBeforeSend } from "./lib/sentry-scrub.js";
|
2026-06-24 11:05:39 +08:00
|
|
|
|
2026-06-28 18:57:53 +08:00
|
|
|
// 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();
|
2026-06-24 11:05:39 +08:00
|
|
|
|
2026-07-10 21:41:49 +08:00
|
|
|
// 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";
|
2026-06-28 18:57:53 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 21:41:49 +08:00
|
|
|
if (ANALYTICS_BAKED.sentryDsn && !telemetryEnvKilled()) {
|
2026-06-24 11:05:39 +08:00
|
|
|
try {
|
|
|
|
|
const Sentry = await import("@sentry/node");
|
|
|
|
|
const { APP_VERSION } = await import("@snapotter/shared");
|
2026-06-29 10:47:30 +08:00
|
|
|
// 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;
|
2026-06-24 11:05:39 +08:00
|
|
|
|
2026-07-10 21:41:49 +08:00
|
|
|
// 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]>;
|
|
|
|
|
|
2026-06-24 11:05:39 +08:00
|
|
|
Sentry.init({
|
|
|
|
|
dsn: ANALYTICS_BAKED.sentryDsn,
|
2026-06-29 10:47:30 +08:00
|
|
|
release,
|
2026-07-10 21:41:49 +08:00
|
|
|
environment: process.env.SNAPOTTER_ENV || "production",
|
2026-06-24 11:05:39 +08:00
|
|
|
sendDefaultPii: false,
|
2026-07-10 21:41:49 +08:00
|
|
|
// 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).
|
2026-07-04 14:02:28 +08:00
|
|
|
integrations: [Sentry.httpIntegration({ trackIncomingRequestsAsSessions: false })],
|
|
|
|
|
sendClientReports: false,
|
2026-07-10 21:41:49 +08:00
|
|
|
maxBreadcrumbs: 0,
|
|
|
|
|
beforeBreadcrumb: () => null,
|
|
|
|
|
initialScope: { tags: { deploy_mode: deployMode() } },
|
|
|
|
|
beforeSend: buildBeforeSend(sentryActive) as unknown as SentryOptions["beforeSend"],
|
2026-06-24 11:05:39 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-10 21:41:49 +08:00
|
|
|
console.log("[sentry] initialized (errors only), release:", release);
|
2026-06-24 11:05:39 +08:00
|
|
|
} catch {
|
|
|
|
|
// @sentry/node not available
|
|
|
|
|
}
|
|
|
|
|
}
|