diff --git a/README.md b/README.md index 2d55cab4f3a..9483d088095 100644 --- a/README.md +++ b/README.md @@ -462,8 +462,8 @@ see [Other model providers](apps/docs/content/docs/providers/model-providers.mdx ## Privacy -Usage analytics are optional and anonymous: product events and a once-a-day count of what is set up, never keys, -names, addresses or conversations. Turn them off in Settings → General → Usage analytics; +Usage analytics are opt-in and anonymous: product events and a once-a-day count of what is set up, never keys, +names, addresses or conversations. They stay off until you turn them on in Settings → General → Usage analytics; [`docs/analytics.md`](docs/analytics.md) lists every event and property. ## Support the project diff --git a/apps/docs/content/docs/security/analytics.mdx b/apps/docs/content/docs/security/analytics.mdx index 58de2b4d36a..4050b5f8473 100644 --- a/apps/docs/content/docs/security/analytics.mdx +++ b/apps/docs/content/docs/security/analytics.mdx @@ -1,12 +1,12 @@ --- title: Usage analytics -description: Every event and property the optional product analytics can send, how to turn it off, and what is never sent. +description: Every event and property the opt-in product analytics can send, how to turn it on or off, and what is never sent. icon: ChartColumn --- MausBot can send anonymous product analytics to PostHog (US cloud, `us.i.posthog.com`), so its maintainers can see -which features people set up and use. This page lists every event and every property the app can send. Nothing else is -sent. +which features people set up and use. It is off until you turn it on. This page lists every event and every property the +app can send. Nothing else is sent. ## What it is @@ -21,10 +21,11 @@ sent. - Events are tied to a random id that PostHog keeps in the page's storage. Nothing ties them to you: the app never sends your email or your name with them. -## Turning it off +## Turning it on or off -**Settings → General → Usage analytics.** When it is off, nothing loads and nothing is sent: no events, no daily -snapshot, and none of the snapshot's requests. +**Settings → General → Usage analytics.** It starts off on every page, including a fresh install and an install that +never touched the switch. When it is off, nothing loads and nothing is sent: no events, no daily snapshot, and none of +the snapshot's requests. The desktop app keeps the switch for this computer's page itself, so it holds whichever port the app serves that page on (it moves to another when its usual one is taken). It is read before anything loads, and before any email is tied to @@ -33,7 +34,7 @@ the app serves its page on that port again, which moves it to the app; while the a port other than its first ties no email to your events. Each server page (your MausBot Cloud, a self-hosted server, each browser you use) keeps its own switch in that page's storage. With the switch off in the desktop app, a server page that the app's window shows, My Cloud's included, starts with it off too once that server runs this version; turning it -on there lasts until that page reloads. In a browser, switch it off on each server's page. +on there lasts until that page reloads. In a browser, each server's page stays off until you switch it on there. ## Never sent diff --git a/docs/analytics.md b/docs/analytics.md index d9ee943ac47..f2b8efa5421 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -1,8 +1,8 @@ # Usage analytics MausBot can send anonymous product analytics to PostHog (US cloud, `us.i.posthog.com`), so its maintainers can see -which features people set up and use. This page lists every event and every property the app can send. Nothing else is -sent. +which features people set up and use. It is off until you turn it on. This page lists every event and every property the +app can send. Nothing else is sent. ## What it is @@ -17,10 +17,11 @@ sent. - Events are tied to a random id that PostHog keeps in the page's storage. Nothing ties them to you: the app never sends your email or your name with them. -## Turning it off +## Turning it on or off -**Settings → General → Usage analytics.** When it is off, nothing loads and nothing is sent: no events, no daily -snapshot, and none of the snapshot's requests. +**Settings → General → Usage analytics.** It starts off on every page, including a fresh install and an install that +never touched the switch. When it is off, nothing loads and nothing is sent: no events, no daily snapshot, and none of +the snapshot's requests. The desktop app keeps the switch for this computer's page itself, so it holds whichever port the app serves that page on (it moves to another when its usual one is taken). It is read before anything loads, and before any email is tied to @@ -29,7 +30,7 @@ the app serves its page on that port again, which moves it to the app; while the a port other than its first ties no email to your events. Each server page (your MausBot Cloud, a self-hosted server, each browser you use) keeps its own switch in that page's storage. With the switch off in the desktop app, a server page that the app's window shows, My Cloud's included, starts with it off too once that server runs this version; turning it -on there lasts until that page reloads. In a browser, switch it off on each server's page. +on there lasts until that page reloads. In a browser, each server's page stays off until you switch it on there. ## Never sent diff --git a/src/lib/analytics.test.ts b/src/lib/analytics.test.ts index f06e39c9385..7c0dcc2b586 100644 --- a/src/lib/analytics.test.ts +++ b/src/lib/analytics.test.ts @@ -63,6 +63,7 @@ vi.stubGlobal("localStorage", baseStorage); beforeEach(() => { store.clear(); + store.set("omb-analytics-opt-out", "0"); ph.loads = 0; ph.fail = false; ph.initThrows = false; @@ -105,8 +106,13 @@ describe("optAction", () => { }); describe("the stored choice", () => { - it("is on for a fresh install", () => { - expect(analyticsEnabled()).toBe(true); + it("is off for a fresh install", async () => { + store.clear(); + const fresh = await freshAnalytics(); + expect(fresh.analyticsEnabled()).toBe(false); + await fresh.initAnalytics(); + expect(ph.loads).toBe(0); + expect(store.get("omb-installed")).toBeUndefined(); }); it("survives a restart once opted out", () => { @@ -138,7 +144,8 @@ describe("the stored choice", () => { expect(store.get("omb-installed")).toBeUndefined(); }); - it("treats unusable storage as a fresh install rather than failing", () => { + it("treats unusable storage as a fresh install rather than failing", async () => { + const fresh = await freshAnalytics(); vi.stubGlobal("localStorage", { getItem: () => { throw new Error("denied"); @@ -147,8 +154,8 @@ describe("the stored choice", () => { throw new Error("denied"); }, }); - expect(analyticsEnabled()).toBe(true); - expect(() => setAnalyticsEnabled(false)).not.toThrow(); + expect(fresh.analyticsEnabled()).toBe(false); + expect(() => fresh.setAnalyticsEnabled(false)).not.toThrow(); }); }); diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index e2db6356fde..f63197a1afa 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -6,7 +6,8 @@ import { readAdvancedMode } from "@/lib/interface-mode"; const TOKEN = "phc_m2hP39w8y2gLPvHgDvSXAu6xcZ3agjf4ruL56rGcMZEe"; -// Read before init(): an opted-out install must never call posthog.init(), so no request leaves the machine. +// Read before init(): an install that has not opted in must never call posthog.init(), so no request leaves the machine. +// Security: analytics is opt-in. Only an explicit "0" (switched on in Settings) turns it on; a missing key is off. const OPT_OUT_KEY = "omb-analytics-opt-out"; // The desktop app's own page is served on whichever of its ports was free, and each port has its own storage. const desktopSwitch = () => globalThis.window?.ogb?.desktopAnalytics; @@ -23,9 +24,9 @@ export function analyticsEnabled(): boolean { const desktop = desktopSwitch()?.enabled(); if (typeof desktop === "boolean") return desktop; try { - return localStorage.getItem(OPT_OUT_KEY) !== "1"; + return localStorage.getItem(OPT_OUT_KEY) === "0"; } catch { - return true; + return false; } } diff --git a/src/lib/analytics.wire.test.ts b/src/lib/analytics.wire.test.ts index 906174d73ca..8c9abc12972 100644 --- a/src/lib/analytics.wire.test.ts +++ b/src/lib/analytics.wire.test.ts @@ -65,6 +65,7 @@ Object.defineProperty(document, "referrer", { configurable: true, value: REFERRE vi.stubGlobal("sessionStorage", memoryStorage()); // What an install that ran a build with save_referrer and save_campaign_params on still holds. vi.stubGlobal("localStorage", memoryStorage({ + "omb-analytics-opt-out": "0", [STORED]: JSON.stringify({ distinct_id: "01a140ba-9e71-7f25-a581-b39f69b5e934", $device_id: "01a140ba-9e71-7f25-a581-b39f69b5e934",