Files
SnapOtter/tests/e2e-analytics/analytics-toggle.spec.ts
T
SnapOtterandGitHub ae6a4c8b7c 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).
2026-07-10 21:41:49 +08:00

117 lines
4.8 KiB
TypeScript

import type { Page } from "@playwright/test";
import { expect, openSettings, test } from "./helpers";
// Proves the instance-wide analytics opt-out works end to end THROUGH THE UI.
// With the bake forced ON, GET /api/v1/config/analytics reports enabled by
// default. An admin flips the System > Privacy toggle off and clicks the real
// "Save Settings" button, and the effective config flips to disabled with every
// secret blanked. The System bulk save now drops the read-only instance_id and
// cookie_secret keys plus any "********" redacted sentinel before the PUT, so
// the Save button persists the opt-out. Previously it 400'd on instance_id
// (READONLY_SETTING) and nothing was written.
//
// The analytics-local Playwright config sets ANALYTICS_BAKED_OVERRIDE=on so the
// compile-time bake is forced ON for this run; without it the effective state
// would be OFF regardless of the toggle (dev/test bake is off by default).
const CONFIG_PATH = "/api/v1/config/analytics";
const ANALYTICS_SWITCH = "Anonymous Product Analytics";
const SAVE_BUTTON = /save settings/i;
interface AnalyticsConfig {
enabled: boolean;
posthogApiKey: string;
posthogHost: string;
sentryDsn: string;
sentryDsnWeb: string;
posthogSampleRate: number;
instanceId: string;
}
async function readConfig(page: Page): Promise<AnalyticsConfig> {
const res = await page.request.get(CONFIG_PATH);
expect(res.ok()).toBeTruthy();
return res.json();
}
async function setAnalyticsEnabled(page: Page, value: "true" | "false"): Promise<void> {
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
const res = await page.request.put("/api/v1/settings", {
headers: token ? { authorization: `Bearer ${token}` } : {},
data: { analyticsEnabled: value },
});
expect(res.ok(), `PUT analyticsEnabled=${value} failed: ${res.status()}`).toBeTruthy();
}
async function openSystemSection(page: Page): Promise<void> {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.getByRole("switch", { name: ANALYTICS_SWITCH })).toBeVisible();
}
test.describe("Analytics opt-out toggle", () => {
// Always reset to ON so a reused dev server and the next spec start enabled,
// even if an assertion above failed mid-test.
test.afterEach(async ({ loggedInPage: page }) => {
await setAnalyticsEnabled(page, "true");
await expect.poll(async () => (await readConfig(page)).enabled, { timeout: 10_000 }).toBe(true);
});
test("admin opts out via the real Save button and the effective config flips off", async ({
loggedInPage: page,
}) => {
// Forced bake ON + no opt-out yet => enabled by default.
expect((await readConfig(page)).enabled).toBe(true);
// Open System settings; the analytics toggle reads ON.
await openSystemSection(page);
const toggle = page.getByRole("switch", { name: ANALYTICS_SWITCH });
await expect(toggle).toHaveAttribute("aria-checked", "true");
// Flip OFF, then persist with the real "Save Settings" button. This is the
// crux: the bulk save must succeed despite GET returning the read-only
// instance_id key and "********" secret sentinels.
await toggle.click();
await expect(toggle).toHaveAttribute("aria-checked", "false");
await page.getByRole("button", { name: SAVE_BUTTON }).click();
// The success message confirms the PUT returned 2xx (no 400 READONLY_SETTING).
await expect(page.getByText("Settings saved.")).toBeVisible();
// The settings route refreshes the gate synchronously on write, so the
// effective config flips to disabled near-immediately, secrets blanked.
await expect
.poll(async () => (await readConfig(page)).enabled, { timeout: 10_000 })
.toBe(false);
expect(await readConfig(page)).toEqual({
enabled: false,
posthogApiKey: "",
posthogHost: "",
sentryDsn: "",
sentryDsnWeb: "",
posthogSampleRate: 0,
instanceId: "",
});
// Reopening Settings shows the toggle OFF (gate -> UI round-trip).
await page.keyboard.press("Escape");
await openSystemSection(page);
await expect(page.getByRole("switch", { name: ANALYTICS_SWITCH })).toHaveAttribute(
"aria-checked",
"false",
);
await page.keyboard.press("Escape");
});
test("targeted opt-out PUT flips the gate off without the UI", async ({ loggedInPage: page }) => {
// Independent of the dialog: a direct analyticsEnabled=false write also
// disables the gate and blanks the secrets. Guards the gate behavior on its
// own so a UI regression can't mask a gate regression.
expect((await readConfig(page)).enabled).toBe(true);
await setAnalyticsEnabled(page, "false");
await expect
.poll(async () => (await readConfig(page)).enabled, { timeout: 10_000 })
.toBe(false);
});
});