mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(analytics): instance census, full capture, richer error context (#511)
Add a once-per-boot instance_started event (arch, os, deploy_mode, gpu_present) so the fleet architecture mix is measurable. It reuses the existing per-instance instance_id and is exempt from the volume sample rate, since a census that fires once per boot must not be thinned. Restore useful capture depth now that the sponsored plan removes the quota pressure behind the earlier hardening: - PostHog sample rate 0.1 to 1.0 (full analytics when enabled); the property allowlist still blocks file data. - Sentry per-instance ceiling 20 to 500/hr, breadcrumb trail restored (sanitized: urls/paths redacted, data payloads dropped), full stack paths kept; local vars, request bodies, and PII still dropped. Both api and web. Honor ANALYTICS_ENABLED=false as an opt-out alias: it was documented on the Docker Hub README but never wired in 2.x, so anyone who set it was still tracked. All capture stays behind the analytics opt-out gate.
This commit is contained in:
@@ -40,21 +40,39 @@ describe("buildBeforeSend (api)", () => {
|
||||
it("returns null when the gate is off", () => {
|
||||
expect(buildBeforeSend(() => false)(evt(), {})).toBeNull();
|
||||
});
|
||||
it("strips PII surfaces and rebuilds the value from the hint error", () => {
|
||||
it("strips high-risk surfaces but keeps full stack paths for debugging", () => {
|
||||
const hint = {
|
||||
originalException: Object.assign(new Error("x"), { code: "EACCES", syscall: "mkdir" }),
|
||||
};
|
||||
const out = send(evt(), hint)!;
|
||||
// Still dropped: these can carry user data / PII.
|
||||
expect(out.message).toBeUndefined();
|
||||
expect(out.server_name).toBeUndefined();
|
||||
expect(out.request).toBeUndefined();
|
||||
expect(out.extra).toBeUndefined();
|
||||
expect(out.breadcrumbs).toBeUndefined();
|
||||
expect(out.user).toBeUndefined();
|
||||
expect(out.exception.values[0].value).toBe("EACCES mkdir");
|
||||
expect(out.exception.values[0].stacktrace.frames[0].filename).toBe("cleanup.ts");
|
||||
expect(out.exception.values[0].stacktrace.frames[0].abs_path).toBeUndefined();
|
||||
expect(out.exception.values[0].stacktrace.frames[0].vars).toBeUndefined();
|
||||
// Restored for debugging: full source path (open-source code, not user data).
|
||||
expect(out.exception.values[0].stacktrace.frames[0].filename).toBe(
|
||||
"/app/apps/api/src/lib/cleanup.ts",
|
||||
);
|
||||
expect(out.exception.values[0].stacktrace.frames[0].abs_path).toBe("/app/x");
|
||||
});
|
||||
it("keeps the breadcrumb trail, redacting paths/urls and dropping data payloads", () => {
|
||||
const out = send(
|
||||
evt({
|
||||
breadcrumbs: [
|
||||
{ message: "GET https://host/u/photo.jpg 200", category: "http", data: { url: "x" } },
|
||||
{ message: "reading /Users/me/secret.txt", category: "console", level: "info" },
|
||||
],
|
||||
}),
|
||||
{},
|
||||
)!;
|
||||
expect(out.breadcrumbs).toEqual([
|
||||
{ message: "GET <url> 200", category: "http" },
|
||||
{ message: "reading <path>", category: "console", level: "info" },
|
||||
]);
|
||||
});
|
||||
it("falls back to type-only for unknown errors", () => {
|
||||
const out = send(evt(), { originalException: new Error("user path /tmp/z") })!;
|
||||
@@ -87,8 +105,8 @@ describe("buildBeforeSend (api)", () => {
|
||||
const out = send(evt({ contexts: { device: { hostname: "leak" } } }), {})!;
|
||||
expect(out.contexts).toBeUndefined();
|
||||
});
|
||||
it("enforces the 20-events-per-hour ceiling", () => {
|
||||
for (let i = 0; i < 20; i++) expect(send(evt(), {})).not.toBeNull();
|
||||
it("enforces the 500-events-per-hour ceiling", () => {
|
||||
for (let i = 0; i < 500; i++) expect(send(evt(), {})).not.toBeNull();
|
||||
expect(send(evt(), {})).toBeNull();
|
||||
});
|
||||
it("never throws on malformed events (fail-closed to a scrubbed event)", () => {
|
||||
|
||||
Reference in New Issue
Block a user