Fix #28: stash stats periodically so beforeunload sends real scores
All checks were successful
CI / build (pull_request) Successful in 32s

Module._analyticsLastStats was only set when the C shutdown path ran
(analytics_session_end). When a player closes the browser tab mid-game,
the beforeunload fallback found _analyticsLastStats null and sent
score 0 to the backend.

Add analytics_stash_stats() which updates _analyticsLastStats every
~1 second during gameplay. The beforeunload handler now always has
current stats to send.
This commit is contained in:
2026-03-19 13:24:21 +00:00
parent c62aae16dc
commit 5517834dd4
3 changed files with 42 additions and 0 deletions

View File

@@ -167,6 +167,20 @@ EM_JS(void, js_analytics_send_end, (int score, int level_reached,
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
});
/* Stash current stats into Module._analyticsLastStats so the
* beforeunload fallback sends real data if the tab is closed. */
EM_JS(void, js_analytics_stash_stats, (int score, int level_reached,
int lives_used, int duration_secs), {
if (!Module._analyticsUrl || !Module._analyticsSessionId) return;
Module._analyticsLastStats = JSON.stringify({
score: score,
level_reached: level_reached > 0 ? level_reached : 1,
lives_used: lives_used,
duration_seconds: duration_secs,
end_reason: 'quit'
});
});
/* ── C wrappers ─────────────────────────────────────────────────── */
void analytics_init(void) {
@@ -188,6 +202,16 @@ void analytics_session_end(GameStats *stats, const char *end_reason) {
);
}
void analytics_stash_stats(GameStats *stats) {
stats_update_score(stats);
js_analytics_stash_stats(
stats->score,
stats->levels_completed > 0 ? stats->levels_completed : 1,
stats->deaths,
(int)stats->time_elapsed
);
}
#else
/* ── Non-WASM stubs ─────────────────────────────────────────────── */
@@ -202,4 +226,8 @@ void analytics_session_end(GameStats *stats, const char *end_reason) {
(void)end_reason;
}
void analytics_stash_stats(GameStats *stats) {
(void)stats;
}
#endif /* __EMSCRIPTEN__ */

View File

@@ -15,4 +15,9 @@ void analytics_session_start(void);
* end_reason: "death", "quit", "timeout", or "completed". */
void analytics_session_end(GameStats *stats, const char *end_reason);
/* Stash current stats for the beforeunload fallback so that
* closing the browser tab sends real data instead of zeros.
* Call periodically from the game loop. No-op on non-WASM. */
void analytics_stash_stats(GameStats *stats);
#endif /* JNR_ANALYTICS_H */

View File

@@ -55,6 +55,8 @@ static int s_mars_depth = 0;
/* ── Analytics / stats tracking ── */
static GameStats s_stats;
static bool s_session_active = false;
static int s_stash_tick = 0; /* frames since last analytics stash */
#define ANALYTICS_STASH_INTERVAL 60 /* stash stats every ~1 second */
/* ── Pause menu state ── */
#define PAUSE_ITEM_COUNT 3
@@ -262,6 +264,7 @@ static void begin_session(void) {
stats_set_active(&s_stats);
analytics_session_start();
s_session_active = true;
s_stash_tick = 0;
}
static void end_session(const char *reason) {
@@ -581,6 +584,12 @@ static void game_update(float dt) {
/* Accumulate play time */
if (s_session_active) {
s_stats.time_elapsed += dt;
/* Periodically stash stats for the beforeunload fallback. */
if (++s_stash_tick >= ANALYTICS_STASH_INTERVAL) {
s_stash_tick = 0;
analytics_stash_stats(&s_stats);
}
}
/* Check for level exit transition */