0
/devs documentation v0.0.1-beta Tracking

SDK · v0.0.1-beta

Tracking

track() — eventos GA4 + tabla events. Aggregator pattern post-run.

track(name, params) es el corazón del analytics del SDK. Dispara un evento custom a:

  1. GA4 (vía window.gtag('event', name, params)) — para análisis ad-hoc en Looker Studio del founder.
  2. Tabla events de Supabase (vía edge function track-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_GAME debe estar seteado antes del track(). Si no, el SDK descarta el evento silenciosamente. Esto es para evitar que un HTML que se olvidó de declarar NEON_GAME contamine la tabla events con filas sin attribuir.

Efectos:

  • GA4: gtag('event', name, params).
  • Supabase: POST track-event con shape:
    {
      "player_id": "<uuid>",
      "session_id": "<uuid>",
      "game": "<NEON_GAME>",
      "event_name": "<name>",
      "params": { ... },
      "client_ts": "2026-04-29T15:23:00.000Z"
    }
  • keepalive: true en el fetch — el evento llega aunque el tab se cierre justo después. Importante para game_over events.

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:

EventoCuándoParams
session_startBoot del SDK(ninguno extra)
profile_nickname_setUser completa el modal de nick{ len: number }
back_to_arcadeClick en el ”← JUGAFY” del topbar{ from: <slug> }
lang_changeCambio 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:

  1. Confirma window.NEON_GAME está seteado antes del primer track.
  2. Confirma que el SDK cargó: console.log(window.NeonArcade?.track) no es undefined.
  3. 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.

Ingresá a Jugafy

Guardá tus scores, subí al leaderboard y sincronizá progreso entre dispositivos.

Contanos

Bug, idea, crítica — todo nos sirve para hacer esto mejor.