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:
@@ -1,16 +1,23 @@
|
||||
import Script from "next/script";
|
||||
|
||||
// Privacy-friendly, cookieless page-view analytics (Umami). Both env vars
|
||||
// unset → render nothing, so local dev and forks never report to our instance.
|
||||
// Privacy-friendly, cookieless analytics (Umami). Both env vars unset →
|
||||
// render nothing, so local dev and forks never report to our instance.
|
||||
// - data-do-not-track: honour the visitor's browser Do Not Track setting.
|
||||
// - data-domains (NEXT_PUBLIC_UMAMI_DOMAINS): extra guard — the tracker only
|
||||
// fires when the live hostname matches, so even a fork that copied our env
|
||||
// vars stays silent on a different domain. Unset → run on all hosts.
|
||||
export function Analytics() {
|
||||
const src = process.env.NEXT_PUBLIC_UMAMI_SRC;
|
||||
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
||||
const domains = process.env.NEXT_PUBLIC_UMAMI_DOMAINS;
|
||||
if (!src || !websiteId) return null;
|
||||
|
||||
return (
|
||||
<Script
|
||||
src={src}
|
||||
data-website-id={websiteId}
|
||||
data-do-not-track="true"
|
||||
{...(domains ? { "data-domains": domains } : {})}
|
||||
strategy="afterInteractive"
|
||||
defer
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { track } from "@/lib/analytics";
|
||||
|
||||
export function CustomForm() {
|
||||
const router = useRouter();
|
||||
@@ -22,6 +23,7 @@ export function CustomForm() {
|
||||
"infiplot:custom",
|
||||
JSON.stringify({ worldSetting, styleGuide }),
|
||||
);
|
||||
track("game_start", { source: "custom" });
|
||||
router.push("/play?custom=1");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user