feat(web): add privacy-friendly Umami custom events

Instrument the play flow with 9 content-free custom events (game_start,
art_style_select, style_image_upload, scene_reached, choice_select,
vision_click, tts_toggle, fullscreen_toggle, play_heartbeat) to measure
retention, engagement depth and session duration.

Privacy is enforced by construction, not convention:
- lib/analytics.ts types each event with a discriminated union, so a
  payload has no slot for free text — prompts, world guides, uploaded
  images and vision output can never reach analytics (compile-time
  guarantee, not a comment).
- track() no-ops without window.umami and never throws into the app.
- coarse 30s heartbeat fires only while the tab is visible.
- script stays gated on NEXT_PUBLIC_UMAMI_* env (blank → no script),
  honours Do-Not-Track, and locks to an exact data-domains allowlist.
- one-line on-site disclosure with a link, shown only when tracking is on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
yuanzonghao
2026-06-04 10:14:08 +08:00
parent 9f4dcc097b
commit 4bf05f6784
6 changed files with 163 additions and 7 deletions
+13 -2
View File
@@ -73,7 +73,18 @@ NEXT_PUBLIC_IMAGE_PROXY_URL=
# NEXT_PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js
# Self-host later: point SRC at your own instance — the integration is identical
# (no code change), e.g. NEXT_PUBLIC_UMAMI_SRC=https://stats.example.com/script.js
# Both blank → no script is injected (zero tracking). NEXT_PUBLIC_ vars are
# inlined at BUILD time, so set them in the build env (Vercel project settings).
# Both blank → no script is injected (zero tracking; every track() call no-ops).
# Beyond page views the app emits content-free custom events (game start, scene
# reached, choice picked, ...) — only enums/counts/booleans, never your prompts,
# uploaded images or any per-user ID. The visitor's Do-Not-Track is honoured.
# NEXT_PUBLIC_ vars are inlined at BUILD time, so set them in the build env
# (Vercel project settings).
NEXT_PUBLIC_UMAMI_SRC=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
# Optional hostname allowlist — defense-in-depth on top of the blank-to-disable
# gate above. The tracker fires only when window.location.hostname EXACTLY
# matches an entry, so a fork that copied these vars stays silent on its own
# domain. Comma-separated, exact match: apex ≠ www (list both), no wildcards.
# Blank → track on all hosts. e.g. infiplot.com,www.infiplot.com
NEXT_PUBLIC_UMAMI_DOMAINS=