1import {
2 EVENT_BATCH_SIZE,
3 SESSION_START_KEY,
4 firstPhotoBucket,
5 type AnalyticsEvent,
6} from "@/lib/analyticsEvents"
7
8/* PRD §4 dan ARCHITECTURE §8: analitik first-party, tanpa pustaka pihak
9 ketiga. Antreannya di memori dan pengirimannya best-effort lewat sendBeacon:
10 saat halaman ditinggalkan, peristiwanya ikut terbang bersama beacon yang
11 sudah diserahkan ke browser. Tidak ada retry dan tidak ada cookie; kalau
12 pengirimannya gagal, peristiwanya memang hilang. */
13
14const ENDPOINT = "/api/events"
15
16let queue: AnalyticsEvent[] = []
17let listenersAttached = false
18
19function flush(): void {
20 if (queue.length === 0) {
21 return
22 }
23 const body = JSON.stringify({ events: queue })
24 queue = []
25 /* sendBeacon adalah jalur utama: beacon yang sudah diserahkan ke browser
26 tetap terkirim walau halamannya ditinggalkan. Blob bertipe
27 application/json sekaligus memenuhi gate jenis isi rutenya. */
28 if (typeof navigator.sendBeacon === "function") {
29 try {
30 if (
31 navigator.sendBeacon(
32 ENDPOINT,
33 new Blob([body], { type: "application/json" })
34 )
35 ) {
36 return
37 }
38 } catch {
39 /* Beacon diblokir atau penuh: pakai fetch di bawah. */
40 }
41 }
42 void fetch(ENDPOINT, {
43 method: "POST",
44 headers: { "content-type": "application/json" },
45 body,
46 keepalive: true,
47 }).catch(() => {})
48}
49
50function attachListeners(): void {
51 if (listenersAttached) {
52 return
53 }
54 listenersAttached = true
55 document.addEventListener("visibilitychange", () => {
56 if (document.visibilityState === "hidden") {
57 flush()
58 }
59 })
60 window.addEventListener("pagehide", flush)
61}
62
63/** Mencatat satu peristiwa. Best-effort: tidak pernah melempar, jadi pemanggil
64 * tidak perlu membungkusnya dengan try/catch (penyimpanan atau sendBeacon
65 * yang diblokir bukan urusan alur foto). */
66export function track(event: AnalyticsEvent): void {
67 try {
68 queue.push(event)
69 attachListeners()
70 if (queue.length >= EVENT_BATCH_SIZE) {
71 flush()
72 }
73 } catch {
74 /* Diam: analitik tidak boleh mengganggu apa pun. */
75 }
76}
77
78/** Bucket durasi Welcome sampai foto pertama dibanding penanda waktu sesi;
79 * null bila sesinya belum ditandai (mis. pengguna masuk langsung ke
80 * /booth/capture dari tautan) atau bila penyimpanan diblokir peramban. */
81export function firstPhotoBucketEvent(): AnalyticsEvent | null {
82 try {
83 const started = Number(window.sessionStorage.getItem(SESSION_START_KEY))
84 if (!Number.isFinite(started) || started <= 0) {
85 return null
86 }
87 return firstPhotoBucket(Date.now() - started)
88 } catch {
89 /* Penyimpanan diblokir (kebijakan perusahaan, mode privat): analitiknya
90 hilang, alur fotonya tidak boleh ikut hilang. */
91 return null
92 }
93}
94
95/** Menandai awal sesi untuk bucket "cepat dipakai" (PRD §4). Gagal menyimpan
96 * bukan urusan pengguna: penandanya saja yang tidak ada. */
97export function markSessionStart(): void {
98 try {
99 window.sessionStorage.setItem(SESSION_START_KEY, String(Date.now()))
100 } catch {
101 /* Diam, sama seperti `track()`. */
102 }
103}
104