2026-06-24 11:05:39 +08:00
|
|
|
import { ANALYTICS_BAKED } from "@snapotter/shared";
|
2026-06-28 18:57:53 +08:00
|
|
|
import { analyticsEnabled, gatePrimed } from "./lib/analytics-gate.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-06-28 18:57:53 +08:00
|
|
|
// Collapse any absolute path in a stack frame filename to its basename, so
|
|
|
|
|
// even our own source paths never carry a workspace or job directory.
|
|
|
|
|
function basename(p: string): string {
|
|
|
|
|
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
|
|
|
return i >= 0 ? p.slice(i + 1) : p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ANALYTICS_BAKED.sentryDsn) {
|
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
|
|
|
|
|
|
|
|
Sentry.init({
|
|
|
|
|
dsn: ANALYTICS_BAKED.sentryDsn,
|
2026-06-29 10:47:30 +08:00
|
|
|
release,
|
2026-06-24 11:05:39 +08:00
|
|
|
environment: process.env.NODE_ENV || "production",
|
|
|
|
|
tracesSampleRate: ANALYTICS_BAKED.sampleRate,
|
|
|
|
|
sendDefaultPii: false,
|
2026-06-28 18:57:53 +08:00
|
|
|
// Runtime opt-out: drop the whole transaction when analytics is off.
|
|
|
|
|
tracesSampler: () => (sentryActive() ? ANALYTICS_BAKED.sampleRate : 0),
|
2026-06-24 11:05:39 +08:00
|
|
|
beforeSend(event) {
|
2026-06-28 18:57:53 +08:00
|
|
|
if (!sentryActive()) return null; // kill switch (covers auto-captured errors)
|
|
|
|
|
// Allow-list: emit only error type + a basename-collapsed stack.
|
|
|
|
|
event.message = undefined;
|
|
|
|
|
event.logentry = undefined; // structured twin of message (captureMessage path)
|
|
|
|
|
event.server_name = undefined; // hostname is not anonymous
|
|
|
|
|
event.request = undefined;
|
|
|
|
|
event.extra = undefined;
|
|
|
|
|
event.contexts = undefined;
|
|
|
|
|
event.breadcrumbs = undefined;
|
|
|
|
|
event.user = undefined;
|
2026-06-24 11:05:39 +08:00
|
|
|
if (event.exception?.values) {
|
|
|
|
|
for (const ex of event.exception.values) {
|
2026-06-28 18:57:53 +08:00
|
|
|
ex.value = ex.type; // never the raw message body
|
2026-06-24 11:05:39 +08:00
|
|
|
if (ex.stacktrace?.frames) {
|
|
|
|
|
for (const frame of ex.stacktrace.frames) {
|
2026-06-28 18:57:53 +08:00
|
|
|
if (frame.filename) frame.filename = basename(frame.filename);
|
|
|
|
|
frame.abs_path = undefined;
|
|
|
|
|
frame.vars = undefined;
|
2026-06-24 11:05:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return event;
|
|
|
|
|
},
|
2026-06-28 18:57:53 +08:00
|
|
|
beforeBreadcrumb() {
|
|
|
|
|
return null; // breadcrumbs can carry URLs/messages with content; drop them
|
|
|
|
|
},
|
|
|
|
|
beforeSendTransaction(event) {
|
|
|
|
|
return sentryActive() ? event : null;
|
2026-06-24 11:05:39 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-29 10:47:30 +08:00
|
|
|
console.log("[sentry] initialized, release:", release);
|
2026-06-24 11:05:39 +08:00
|
|
|
} catch {
|
|
|
|
|
// @sentry/node not available
|
|
|
|
|
}
|
|
|
|
|
}
|