track(name, params) es el corazón del analytics del SDK. Dispara un evento custom a:
- GA4 (vía
window.gtag('event', name, params)) — para análisis ad-hoc en Looker Studio del founder. - Tabla
eventsde Supabase (vía edge functiontrack-event) — fuente de verdad para queries SQL custom + leaderboards segmentados por evento.
Es fire-and-forget: no await, no throw. Si un ad-blocker corta GA4, o si la red corta el POST a Supabase, tu juego no se entera. La idea es que track() nunca rompa el game loop.
API
track(name, params?): void
import { track } from "/sdk/v0.0.1-beta/jugafy.js";
track("session_start"); // sin params
track("score_bump", { score: 5, combo: 3 });
track("chest_opened", { tier: "gold", level: 7, time_in_run_ms: 23400 });
Requisitos:
name: string non-empty. Convención:snake_case.params: object opcional. Cualquier shape JSON-serializable. Numbers, strings, booleans, arrays, nested objects.window.NEON_GAMEdebe estar seteado antes deltrack(). Si no, el SDK descarta el evento silenciosamente. Esto es para evitar que un HTML que se olvidó de declararNEON_GAMEcontamine la tablaeventscon filas sin attribuir.
Efectos:
- GA4:
gtag('event', name, params). - Supabase: POST
track-eventcon shape:{ "player_id": "<uuid>", "session_id": "<uuid>", "game": "<NEON_GAME>", "event_name": "<name>", "params": { ... }, "client_ts": "2026-04-29T15:23:00.000Z" } keepalive: trueen el fetch — el evento llega aunque el tab se cierre justo después. Importante paragame_overevents.
No tira errores. Si el params tiene un cycle (obj.self = obj), el JSON.stringify interno tira en el try/catch y el evento se descarta.
Eventos auto-disparados por el SDK
El SDK ya dispara algunos eventos solo. No los re-emitas:
| Evento | Cuándo | Params |
|---|---|---|
session_start | Boot del SDK | (ninguno extra) |
profile_nickname_set | User completa el modal de nick | { len: number } |
back_to_arcade | Click en el ”← JUGAFY” del topbar | { from: <slug> } |
lang_change | Cambio de idioma via mountLangToggle | { from, to } |
Convenciones de naming
Eventos del lifecycle de tu juego
<slug>_run_started // empezar una run
<slug>_run_ended // terminar (winner o game over). El SDK detecta esto
// y postMessages al parent na:game-ended.
<slug>_game_over // alias de _run_ended si tu juego tiene la dicotomía
// "winner" vs "game over" separada. El parent recibe
// ambos como na:game-ended.
El SDK tiene una whitelist hard-coded de eventos terminales que disparan el bridge al parent:
/^(anima|pulso|penguin|bubble)_(game_over|run_ended)$/
Cuando subas tu juego con un slug nuevo, vamos a actualizar esa regex (es trivial) o evolucionarla a un pattern más permisivo. Por ahora, contactá al founder después de aprobar tu juego para que se incluya. Sin esto, <tu-slug>_run_ended se trackea normal pero el modal “no pierdas tu progreso” del parent no se dispara.
Eventos custom de tu juego
Convención: prefijo con tu slug para evitar colisiones cross-game.
track("anima_perfect_combo", { length: 12 });
track("pulso_card_picked", { card: "magnet", level: 3 });
track("mi-juego_secret_found", { area: "cave_3" });
Si tu juego usa el slug en el evento (lo recomendamos), no tenés que pasarlo en params — ya viene en event.game del payload.
Aggregator pattern (recomendado)
No mandes 1 evento por click/frame/spawn. Eso satura la tabla events, sube el costo de Supabase y hace los queries más lentos.
Mandá 1 evento gordo al final de cada run (un “aggregator”) con todos los counters de esa run.
Mal
// 1 evento por gem ⇒ una run de 200 gems = 200 events
function onGemPickup() {
track("gem_pickup", { x: gem.x, y: gem.y });
}
Bien
// 1 evento por run con TODO sumarizado
const stats = { gems: 0, kills: 0, max_combo: 0, hp_left: 0 };
function onGemPickup() { stats.gems++; }
function onKill() { stats.kills++; }
function onCombo(n) { stats.max_combo = Math.max(stats.max_combo, n); }
function onRunEnded(score, durationMs) {
track(`${slug}_run_ended`, {
score,
duration_ms: durationMs,
...stats,
});
}
Detalle del pattern + ejemplos del repo en MEMORY.md → “Estándar de instrumentación”.
Eventos que SÍ dispará por unidad
Los aggregators NO aplican a eventos que representan decisiones puntuales del jugador que querés analizar separado:
track("anima_card_picked", { card: "shockwave", level: 3, hp_at_pick: 8 });
track("pulso_chest_opened", { tier: "gold", time_in_run_ms: 23400 });
Cada decisión = 1 evento. Estos te dan data para tunear curvas de progresión / drop rates / etc.
Si tu juego tiene 50 chests por run, sí, son 50 events por run. OK — eso es señal real de comportamiento, no spam.
Verificar tracking en dev local
Abrí DevTools → Network → filtrá track-event. Cada track() te tiene que tirar un POST con status 200 (o 401 si tenés sesión vieja).
Si NO aparecen requests:
- Confirma
window.NEON_GAMEestá seteado antes del primer track. - Confirma que el SDK cargó:
console.log(window.NeonArcade?.track)no esundefined. - Si usás Brave/Firefox con tracking protection, GA4 puede estar bloqueado pero el POST a Supabase debería pasar igual. Si TODO está bloqueado, es ad-blocker total — apagálo para dev.
Verificar tracking en prod (post-aprobación)
Subite el bundle, aprobalo, andá al juego en https://jugafy.com/<categoría>/<slug> y jugá una run. Después query a Supabase:
SELECT event_name, COUNT(*), MAX(server_ts)
FROM public.events
WHERE game = 'tu-slug' AND server_ts > now() - interval '1 hour'
GROUP BY event_name
ORDER BY 3 DESC;
Tenés que ver al menos session_start y <slug>_run_ended con timestamps recientes.
Privacy
Todos los eventos se mandan con un player_id UUID anon (no es email ni IP). Si el visitor está logueado en jugafy.com, además se adjunta el access_token y la edge function correlaciona a user_id. No mandes PII en params (emails, nombres reales, IPs). Si tenés duda sobre qué mandar, preguntá.
params se persiste como JSONB en la tabla. Dato sensible nunca debería entrar.