forked from tas/major_tom
Fix review issues in analytics integration
1. Race condition: session_end now waits for an in-flight session_start promise before sending, so quick restarts don't drop the end call. 2. beforeunload: replaced sendBeacon (which can't set headers) with fetch(..., keepalive: true) so the X-API-Key header is included and the backend doesn't 401. 3. Stats double-counting: removed stats_record_damage_dealt and stats_record_kill from damage_entity (which was called for all damage including player deaths). Now only recorded at player-sourced call sites (projectile hits, stomps). 4. Removed const-cast: analytics_session_end now takes GameStats* (non-const) since stats_update_score mutates it. 5. beforeunload now uses stashed stats from the last C-side session_end call instead of hardcoded zeroes. Session ID is cleared synchronously before async fetch to prevent races. 6. Removed unused stdint.h include from stats.h.
This commit is contained in:
@@ -18,6 +18,8 @@ EM_JS(void, js_analytics_init, (), {
|
||||
* -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: check for data attributes on the canvas container
|
||||
* first, then fall back to compiled-in defaults. */
|
||||
@@ -36,7 +38,8 @@ EM_JS(void, js_analytics_init, (), {
|
||||
}
|
||||
});
|
||||
|
||||
/* Start a new session. Sends POST /api/analytics/session/start/. */
|
||||
/* 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;
|
||||
|
||||
@@ -64,7 +67,7 @@ EM_JS(void, js_analytics_session_start, (), {
|
||||
}
|
||||
});
|
||||
|
||||
fetch(Module._analyticsUrl + '/api/analytics/session/start/', {
|
||||
Module._analyticsStartPending = fetch(Module._analyticsUrl + '/api/analytics/session/start/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -75,50 +78,77 @@ EM_JS(void, js_analytics_session_start, (), {
|
||||
.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);
|
||||
});
|
||||
});
|
||||
|
||||
/* End the current session with gameplay stats.
|
||||
* Parameters are passed from C as integers/string. */
|
||||
EM_JS(void, js_analytics_session_end, (int score, int level_reached,
|
||||
int lives_used, int duration_secs,
|
||||
const char *end_reason_ptr), {
|
||||
if (!Module._analyticsUrl || !Module._analyticsSessionId) return;
|
||||
/* 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;
|
||||
|
||||
var body = JSON.stringify({
|
||||
score: score,
|
||||
level_reached: level_reached > 0 ? level_reached : 1,
|
||||
lives_used: lives_used,
|
||||
duration_seconds: duration_secs,
|
||||
end_reason: endReason
|
||||
});
|
||||
|
||||
fetch(Module._analyticsUrl + '/api/analytics/session/' + sid + '/end/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': Module._analyticsKey
|
||||
},
|
||||
body: body
|
||||
})
|
||||
.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);
|
||||
});
|
||||
|
||||
/* Clear session so duplicate end calls are harmless */
|
||||
/* Clear synchronously before the async request to prevent races */
|
||||
Module._analyticsSessionId = null;
|
||||
doEnd(sid, endReason, score, level_reached, lives_used, duration_secs);
|
||||
});
|
||||
|
||||
/* ── C wrappers ─────────────────────────────────────────────────── */
|
||||
@@ -131,9 +161,9 @@ void analytics_session_start(void) {
|
||||
js_analytics_session_start();
|
||||
}
|
||||
|
||||
void analytics_session_end(const GameStats *stats, const char *end_reason) {
|
||||
stats_update_score((GameStats *)stats);
|
||||
js_analytics_session_end(
|
||||
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,
|
||||
@@ -151,7 +181,7 @@ void analytics_init(void) {
|
||||
|
||||
void analytics_session_start(void) {}
|
||||
|
||||
void analytics_session_end(const GameStats *stats, const char *end_reason) {
|
||||
void analytics_session_end(GameStats *stats, const char *end_reason) {
|
||||
(void)stats;
|
||||
(void)end_reason;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user