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:
@@ -219,7 +219,8 @@ describe("analytics lib (baked model)", () => {
|
||||
expect(result.user).toBeUndefined();
|
||||
expect(result.message).toBeUndefined();
|
||||
expect(result.request).toBeUndefined();
|
||||
expect(result.breadcrumbs).toBeUndefined();
|
||||
// Breadcrumbs are kept for debugging but sanitized (paths/urls redacted).
|
||||
expect(result.breadcrumbs).toEqual([{ message: "<path>" }]);
|
||||
// The exception message is replaced by its type so no free text leaves.
|
||||
expect(result.exception.values[0].value).toBe("TypeError");
|
||||
// Filesystem paths collapse to the basename (directory and any username
|
||||
@@ -248,64 +249,4 @@ describe("analytics lib (baked model)", () => {
|
||||
expect(result.exception.values[0].value).toBe("RangeError");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sentry beforeBreadcrumb callback", () => {
|
||||
async function getBeforeBreadcrumb() {
|
||||
mockSentryInit.mockClear();
|
||||
await mod.initAnalytics(enabledConfig);
|
||||
const sentryCall = mockSentryInit.mock.calls.find(
|
||||
(call: unknown[]) => call[0]?.beforeBreadcrumb,
|
||||
);
|
||||
return sentryCall ? sentryCall[0].beforeBreadcrumb : null;
|
||||
}
|
||||
|
||||
it("returns null for ui.click breadcrumbs", async () => {
|
||||
const beforeBreadcrumb = await getBeforeBreadcrumb();
|
||||
if (!beforeBreadcrumb) return;
|
||||
|
||||
const result = beforeBreadcrumb({ category: "ui.click" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for fetch breadcrumbs with file extension URLs", async () => {
|
||||
const beforeBreadcrumb = await getBeforeBreadcrumb();
|
||||
if (!beforeBreadcrumb) return;
|
||||
|
||||
const result = beforeBreadcrumb({
|
||||
category: "fetch",
|
||||
data: { url: "https://example.com/uploads/photo.png" },
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("drops console breadcrumbs even with file paths", async () => {
|
||||
const beforeBreadcrumb = await getBeforeBreadcrumb();
|
||||
if (!beforeBreadcrumb) return;
|
||||
|
||||
const result = beforeBreadcrumb({
|
||||
category: "console",
|
||||
message: "Error loading /tmp/workspace/file.jpg",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("drops fetch breadcrumbs to non-file URLs too", async () => {
|
||||
const beforeBreadcrumb = await getBeforeBreadcrumb();
|
||||
if (!beforeBreadcrumb) return;
|
||||
|
||||
const result = beforeBreadcrumb({
|
||||
category: "fetch",
|
||||
data: { url: "https://example.com/api/v1/health" },
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("drops navigation breadcrumbs without a message", async () => {
|
||||
const beforeBreadcrumb = await getBeforeBreadcrumb();
|
||||
if (!beforeBreadcrumb) return;
|
||||
|
||||
const result = beforeBreadcrumb({ category: "navigation" });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user