#include "game/analytics.h" #include #ifdef __EMSCRIPTEN__ #include /* ── EM_JS bridge: JavaScript functions callable from C ─────────── */ /* Initialize client_id in localStorage and store the analytics * API URL + key. Called once at startup. */ EM_JS(void, js_analytics_init, (), { /* Generate or retrieve a persistent client UUID */ if (!localStorage.getItem('jnr_client_id')) { localStorage.setItem('jnr_client_id', crypto.randomUUID()); } /* Store config on the Module for later use by other EM_JS calls. * ANALYTICS_URL and ANALYTICS_KEY are replaced at build time via * -D defines, falling back to sensible defaults. */ Module._analyticsClientId = localStorage.getItem('jnr_client_id'); Module._analyticsSessionId = null; Module._analyticsStartPending = null; /* Promise while start is in-flight */ Module._analyticsLastStats = null; /* stashed for beforeunload fallback */ /* Runtime config: URL from data attribute, key decoded at runtime. * The key is XOR-encoded across two byte arrays so it never appears * as a plain string in the WASM binary, emitted JS, or HTML. */ var container = document.getElementById('canvas-container'); Module._analyticsUrl = (container && container.dataset.analyticsUrl) ? container.dataset.analyticsUrl : (typeof ANALYTICS_URL !== 'undefined' ? ANALYTICS_URL : ''); var _a = [53,75,96,19,114,122,112,34,28,62,24,5,57,34,126,14, 112,73,105,121,122,79,50,0,77,33,82,58,61,19,44,0]; var _b = [82,15,4,95,36,32,29,18,95,14,87,95,115,70,12,76, 55,5,4,12,28,30,65,78,4,72,26,92,84,90,70,54]; var _k = ''; for (var i = 0; i < _a.length; i++) _k += String.fromCharCode(_a[i] ^ _b[i]); Module._analyticsKey = _k; if (!Module._analyticsUrl) { console.log('[analytics] No analytics URL configured — analytics disabled'); } else { console.log('[analytics] Initialized, client_id=' + Module._analyticsClientId); } }); /* Start a new session. Sends POST /api/analytics/session/start/. * Stores the in-flight promise so session_end can wait for it. */ EM_JS(void, js_analytics_session_start, (), { if (!Module._analyticsUrl) return; var body = JSON.stringify({ client_id: Module._analyticsClientId, device: { platform: navigator.platform || '', language: navigator.language || '', screen_width: screen.width, screen_height: screen.height, device_pixel_ratio: window.devicePixelRatio || 1, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '', webgl_renderer: (function() { try { var c = document.createElement('canvas'); var gl = c.getContext('webgl') || c.getContext('experimental-webgl'); if (gl) { var ext = gl.getExtension('WEBGL_debug_renderer_info'); return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : ''; } } catch(e) {} return ''; })(), touch_support: ('ontouchstart' in window) } }); Module._analyticsStartPending = fetch(Module._analyticsUrl + '/api/analytics/session/start/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': Module._analyticsKey }, body: body }) .then(function(r) { return r.json(); }) .then(function(data) { Module._analyticsSessionId = data.session_id || null; Module._analyticsStartPending = null; console.log('[analytics] Session started: ' + Module._analyticsSessionId); }) .catch(function(err) { Module._analyticsStartPending = null; console.error('[analytics] Session start failed:', err); }); }); /* Internal helper: send the session-end POST (used by both the C wrapper * and the beforeunload fallback). */ EM_JS(void, js_analytics_send_end, (int score, int level_reached, int lives_used, int duration_secs, const char *end_reason_ptr), { /* Helper that performs the actual end request given a session id. */ function doEnd(sid, endReason, score, levelReached, livesUsed, durationSecs) { var body = JSON.stringify({ score: score, level_reached: levelReached > 0 ? levelReached : 1, lives_used: livesUsed, duration_seconds: durationSecs, end_reason: endReason }); /* Stash stats for the beforeunload fallback */ Module._analyticsLastStats = body; return fetch(Module._analyticsUrl + '/api/analytics/session/' + sid + '/end/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': Module._analyticsKey }, body: body, keepalive: true }) .then(function(r) { return r.json(); }) .then(function(data) { console.log('[analytics] Session ended: ' + sid + (data.new_high_score ? ' (NEW HIGH SCORE!)' : '')); }) .catch(function(err) { console.error('[analytics] Session end failed:', err); }); } if (!Module._analyticsUrl) return; var endReason = UTF8ToString(end_reason_ptr); /* If session start is still in-flight, wait for it before ending. */ if (Module._analyticsStartPending) { var pending = Module._analyticsStartPending; /* Clear session synchronously so duplicate end calls are harmless */ Module._analyticsStartPending = null; pending.then(function() { var sid = Module._analyticsSessionId; if (sid) { Module._analyticsSessionId = null; doEnd(sid, endReason, score, level_reached, lives_used, duration_secs); } }); return; } if (!Module._analyticsSessionId) return; var sid = Module._analyticsSessionId; /* Clear synchronously before the async request to prevent races */ Module._analyticsSessionId = null; doEnd(sid, endReason, score, level_reached, lives_used, duration_secs); }); /* ── C wrappers ─────────────────────────────────────────────────── */ void analytics_init(void) { js_analytics_init(); } void analytics_session_start(void) { js_analytics_session_start(); } void analytics_session_end(GameStats *stats, const char *end_reason) { stats_update_score(stats); js_analytics_send_end( stats->score, stats->levels_completed > 0 ? stats->levels_completed : 1, stats->deaths, (int)stats->time_elapsed, end_reason ); } #else /* ── Non-WASM stubs ─────────────────────────────────────────────── */ void analytics_init(void) { printf("[analytics] Analytics disabled (native build)\n"); } void analytics_session_start(void) {} void analytics_session_end(GameStats *stats, const char *end_reason) { (void)stats; (void)end_reason; } #endif /* __EMSCRIPTEN__ */