IKHSAN
ENID Baca dalam Bahasa Indonesia

snapme

Public
Branch
master
Latest commit
130fdb828 October 2026

analytics.ts

Open raw
import {
  EVENT_BATCH_SIZE,
  SESSION_START_KEY,
  firstPhotoBucket,
  type AnalyticsEvent,
} from "@/lib/analyticsEvents"

/* PRD §4 dan ARCHITECTURE §8: analitik first-party, tanpa pustaka pihak
   ketiga. Antreannya di memori dan pengirimannya best-effort lewat sendBeacon:
   saat halaman ditinggalkan, peristiwanya ikut terbang bersama beacon yang
   sudah diserahkan ke browser. Tidak ada retry dan tidak ada cookie; kalau
   pengirimannya gagal, peristiwanya memang hilang. */

const ENDPOINT = "/api/events"

let queue: AnalyticsEvent[] = []
let listenersAttached = false

function flush(): void {
  if (queue.length === 0) {
    return
  }
  const body = JSON.stringify({ events: queue })
  queue = []
  /* sendBeacon adalah jalur utama: beacon yang sudah diserahkan ke browser
     tetap terkirim walau halamannya ditinggalkan. Blob bertipe
     application/json sekaligus memenuhi gate jenis isi rutenya. */
  if (typeof navigator.sendBeacon === "function") {
    try {
      if (
        navigator.sendBeacon(
          ENDPOINT,
          new Blob([body], { type: "application/json" })
        )
      ) {
        return
      }
    } catch {
      /* Beacon diblokir atau penuh: pakai fetch di bawah. */
    }
  }
  void fetch(ENDPOINT, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body,
    keepalive: true,
  }).catch(() => {})
}

function attachListeners(): void {
  if (listenersAttached) {
    return
  }
  listenersAttached = true
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
      flush()
    }
  })
  window.addEventListener("pagehide", flush)
}

/** Mencatat satu peristiwa. Best-effort: tidak pernah melempar, jadi pemanggil
 *  tidak perlu membungkusnya dengan try/catch (penyimpanan atau sendBeacon
 *  yang diblokir bukan urusan alur foto). */
export function track(event: AnalyticsEvent): void {
  try {
    queue.push(event)
    attachListeners()
    if (queue.length >= EVENT_BATCH_SIZE) {
      flush()
    }
  } catch {
    /* Diam: analitik tidak boleh mengganggu apa pun. */
  }
}

/** Bucket durasi Welcome sampai foto pertama dibanding penanda waktu sesi;
 *  null bila sesinya belum ditandai (mis. pengguna masuk langsung ke
 *  /booth/capture dari tautan) atau bila penyimpanan diblokir peramban. */
export function firstPhotoBucketEvent(): AnalyticsEvent | null {
  try {
    const started = Number(window.sessionStorage.getItem(SESSION_START_KEY))
    if (!Number.isFinite(started) || started <= 0) {
      return null
    }
    return firstPhotoBucket(Date.now() - started)
  } catch {
    /* Penyimpanan diblokir (kebijakan perusahaan, mode privat): analitiknya
       hilang, alur fotonya tidak boleh ikut hilang. */
    return null
  }
}

/** Menandai awal sesi untuk bucket "cepat dipakai" (PRD §4). Gagal menyimpan
 *  bukan urusan pengguna: penandanya saja yang tidak ada. */
export function markSessionStart(): void {
  try {
    window.sessionStorage.setItem(SESSION_START_KEY, String(Date.now()))
  } catch {
    /* Diam, sama seperti `track()`. */
  }
}
bytes
3,103
lines
104
Type
ts
Latest commit
28 October 2026