mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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).
This commit is contained in:
@@ -47,7 +47,8 @@ const enabledConfig = {
|
||||
posthogApiKey: "phc_test",
|
||||
posthogHost: "https://ph.test",
|
||||
sentryDsn: "https://sentry.test/123",
|
||||
sampleRate: 1,
|
||||
sentryDsnWeb: "https://sentry.test/web/456",
|
||||
posthogSampleRate: 1,
|
||||
instanceId: "inst-1",
|
||||
};
|
||||
|
||||
@@ -56,7 +57,8 @@ const disabledConfig = {
|
||||
posthogApiKey: "",
|
||||
posthogHost: "",
|
||||
sentryDsn: "",
|
||||
sampleRate: 0,
|
||||
sentryDsnWeb: "",
|
||||
posthogSampleRate: 0,
|
||||
instanceId: "",
|
||||
};
|
||||
|
||||
@@ -68,6 +70,7 @@ describe("Analytics No-Leak Invariant (baked model)", () => {
|
||||
mockPosthogInit.mockClear();
|
||||
mockCapture.mockClear();
|
||||
mockSentryInit.mockClear();
|
||||
mockBrowserTracingIntegration.mockClear();
|
||||
vi.resetModules();
|
||||
mod = await import("../../../apps/web/src/lib/analytics");
|
||||
});
|
||||
@@ -103,18 +106,75 @@ describe("Analytics No-Leak Invariant (baked model)", () => {
|
||||
expect(mockCapture).toHaveBeenCalledWith("tool_opened", { tool_id: "resize" });
|
||||
});
|
||||
|
||||
it("initAnalytics initializes Sentry when sentryDsn provided", async () => {
|
||||
it("initAnalytics initializes Sentry with the web DSN when sentryDsnWeb provided", async () => {
|
||||
await mod.initAnalytics(enabledConfig);
|
||||
expect(mockSentryInit).toHaveBeenCalledOnce();
|
||||
expect(mockSentryInit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dsn: "https://sentry.test/123",
|
||||
dsn: "https://sentry.test/web/456",
|
||||
sendDefaultPii: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors-only web Sentry init", () => {
|
||||
async function getSentryOptions() {
|
||||
await mod.initAnalytics(enabledConfig);
|
||||
return mockSentryInit.mock.calls[0]?.[0];
|
||||
}
|
||||
|
||||
it("passes no tracesSampleRate and never constructs a tracing integration", async () => {
|
||||
const options = await getSentryOptions();
|
||||
expect(options).toBeDefined();
|
||||
expect("tracesSampleRate" in options).toBe(false);
|
||||
expect("tracesSampler" in options).toBe(false);
|
||||
expect(mockBrowserTracingIntegration).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables SDK client reports", async () => {
|
||||
const options = await getSentryOptions();
|
||||
expect(options.sendClientReports).toBe(false);
|
||||
});
|
||||
|
||||
it("filters the release-health session integration out of the defaults", async () => {
|
||||
const options = await getSentryOptions();
|
||||
const filtered = options.integrations([{ name: "BrowserSession" }, { name: "Dedupe" }]);
|
||||
expect(filtered).toEqual([{ name: "Dedupe" }]);
|
||||
});
|
||||
|
||||
it("does not init Sentry when sentryDsnWeb is empty even if sentryDsn is set", async () => {
|
||||
await mod.initAnalytics({ ...enabledConfig, sentryDsnWeb: "" });
|
||||
expect(mockSentryInit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("web-DSN-only init (no PostHog key)", () => {
|
||||
const sentryOnlyConfig = { ...enabledConfig, posthogApiKey: "", posthogHost: "" };
|
||||
|
||||
it("skips posthog.init entirely but still initializes Sentry", async () => {
|
||||
await mod.initAnalytics(sentryOnlyConfig);
|
||||
expect(mockPosthogInit).not.toHaveBeenCalled();
|
||||
expect(mockSentryInit).toHaveBeenCalledOnce();
|
||||
expect(mockSentryInit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ dsn: "https://sentry.test/web/456" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps beforeSend active: error events still pass the enabled gate", async () => {
|
||||
await mod.initAnalytics(sentryOnlyConfig);
|
||||
const beforeSend = mockSentryInit.mock.calls[0]?.[0]?.beforeSend;
|
||||
expect(beforeSend).toBeDefined();
|
||||
const result = beforeSend({
|
||||
exception: { values: [{ type: "TypeError", value: "boom" }] },
|
||||
});
|
||||
// A null here would mean the enabled gate never opened for a web-DSN-only
|
||||
// bake; the event must survive (scrubbed to type-only).
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.exception.values[0].value).toBe("TypeError");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PII never leaks even when analytics enabled", () => {
|
||||
it("Sentry strips the entire user object from events", async () => {
|
||||
await mod.initAnalytics(enabledConfig);
|
||||
|
||||
Reference in New Issue
Block a user