Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/_components/analytics-gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SCRIPT_ID = "ga-gtag-js"
* without an explanation.
*/
const FALLBACK_TEXT =
"We use cookies to measure how this site is used, via Google Analytics. Analytics only runs if you accept."
"Can we count your visit? It shows us which pages actually help people — we only ever look at totals, and never use it for ads. It runs on Google Analytics cookies, and only if you accept."

type Consent = "granted" | "denied"

Expand Down
9 changes: 9 additions & 0 deletions docs/notes/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- `packages/db/src/schema.ts` — `analyticsConfig` table (`analytics_config`), single row per `kind` (`google_analytics` today), same shape-pattern as `emailConfig`. `measurementId` plaintext (public id, not a secret). Satisfies `L2-ANALYTICS-01`.
- `packages/db/migrations/0005_clear_mattie_franklin.sql` — drizzle-kit generated `CREATE TABLE analytics_config`.
- `packages/db/migrations/0006_seed_analytics_config.sql` — hand-written custom migration (`drizzle-kit generate --custom`), seeds the singleton row + default banner copy, `ON CONFLICT ("kind") DO NOTHING`. Fixed literal uuid for the seed id (the pk has no DB-level default — `$defaultFn` is app-side only). Satisfies `L2-ANALYTICS-01`, `L2-ANALYTICS-08`.
- `packages/db/migrations/0008_reword_cookie_banner.sql` — custom migration; rewrites the banner copy seeded by 0006. Guarded `WHERE "cookieBannerText" = <the 0006 literal>`, so an operator-edited banner is never clobbered (`L2-ANALYTICS-08`) and a re-run matches nothing. 0006 is left untouched — applied migrations are immutable; a fresh DB runs 0006 then 0008 and lands on the new copy.
- `settings/_actions.ts` → `saveAnalyticsConfig` — auth + `canAccessSettings` gate → uppercase/validate id → upsert on `kind` → `revalidatePath("/backflip/settings")`. Mirrors `saveEmailConfig` minus encryption. Satisfies `L2-ANALYTICS-02`, `L2-ANALYTICS-12`, `L2-ANALYTICS-14/15`.
- `settings/_components/analytics-integration.tsx` — client detail pane; `useActionState(saveAnalyticsConfig)`; exports the `AnalyticsConfig` view-model type (page.tsx imports it type-only). Banner switch is controlled state so the copy field and the explainer react live. Satisfies `L2-ANALYTICS-05`.
- `settings/_components/integrations-view.tsx` — `Selection` widened to `"ai" | "email" | "analytics"`; third `ListRow` (GA tile, subtitle = the measurement id when set, else "Not configured"); detail switch became a chain.
Expand All @@ -17,6 +18,14 @@
- `apps/web/app/_components/cookie-banner.tsx` — client; presentational only (`role="region"`, `aria-label="Cookie consent"`, fixed bottom, `z-50`, Card-ish bar, `Button` outline/primary). No storage or GA knowledge.
- `apps/web/app/_components/site-footer.tsx` — now renders `<AnalyticsGate />`.

## Banner copy (why it reads the way it does)
Default is a conversion problem, not a legal one — the consent gate is strict (`L2-ANALYTICS-06`), so *undecided* visitors are never measured either, and most people never click anything. Measured traffic ≈ accept rate.
- Old seed led with the cost ("We use cookies … via Google Analytics") and gave no reason to say yes.
- Current default asks for the visit, states the benefit, bounds the use ("we only ever look at totals, and never use it for ads"), then names Google Analytics + cookies. Same facts, cost last.
- Claims are about *our* use only. Don't add promises about what Google does with it — an operator can enable Google Signals/ads features and make them false.
- Layout stays symmetric: Decline and Accept, one click each, same bar. EDPB requires reject to be equally easy; it does not require identical styling, so primary Accept + outline Decline stays compliant. Never remove Decline, never bury it behind a second layer.
- Copy is operator-editable at `/backflip/settings`, so retuning needs no deploy. `FALLBACK_TEXT` in `analytics-gate.tsx` mirrors the seed and must be updated with it.

## Why the config is fetched, not server-rendered
Public pages (`/`, `/getting-started`, both guides) build as `○` static. Reading `analytics_config` in those pages — or in the root layout — would flip them to `ƒ`, and the root layout is shared with `/backflip`, so it would drag the whole app dynamic.

Expand Down
12 changes: 12 additions & 0 deletions packages/db/migrations/0008_reword_cookie_banner.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Custom SQL migration file, put your code below! --
-- Reword the default cookie-banner copy. The 0006 seed led with the cost
-- ("cookies", "Google Analytics") and gave no reason to accept, so most
-- visitors declined or ignored it and nothing was ever measured.
--
-- Only rewrites rows still carrying the untouched 0006 text: the copy is
-- operator-editable (`L2-ANALYTICS-08`), and a migration must never clobber
-- an operator's edit. Idempotent — a second run matches nothing.
UPDATE "analytics_config"
SET "cookieBannerText" = 'Can we count your visit? It shows us which pages actually help people — we only ever look at totals, and never use it for ads. It runs on Google Analytics cookies, and only if you accept.'
WHERE "kind" = 'google_analytics'
AND "cookieBannerText" = 'We use cookies to measure how this site is used, via Google Analytics. Analytics only runs if you accept.';
Loading