Game statistics
The game measures play on our own race server, not through a third-party SDK: every player already has
an account there (a guest one at first start), and the server already settles every career race, confirms
every rewarded ad and credits every gold pack. One D1 table, event (migration 0017_event.sql), gets a
row per thing a player did; npm run stats turns it into the reports. Chosen over Firebase Analytics and
GameAnalytics on 2026-11-12: the reports that matter most (days to each boss with and without ads) are one
SQL join here, and would need the BigQuery export there.
What is logged
Section titled “What is logged”The server logs what it decides itself, so a modified game cannot fake it:
| Event | Where | Data |
|---|---|---|
race |
api/raceApi.ts, each judged career, quick or weekly race |
race id, round, won, blown, stars, first win, boss, cash (prize plus wager), gold, bet |
ad |
a rewarded ad’s reward paid (api/actionEvents.ts, api/raceApi.ts) |
placement: dailyBonus, fuelAd, goldAd, double, keepRounds |
action |
every garage action that went through (api/garageApi.ts) |
action type, the car or part, cash and gold change |
purchase, refund |
a gold pack credited or taken back (db/creditedPurchases.ts) |
product, gold |
The game sends the rest (player/account/analytics.ts, names in analyticsEvents.ts), in batches every
30 seconds and when the app goes to the background, to POST /api/events:
| Event | Where | Data |
|---|---|---|
session |
boot, and back in the app after 30 minutes away | build, target, platform |
beat |
a story beat seen for the first time (player/story/seen.ts) |
beat id |
ad_offer |
a rewarded ad button or choice shown (ui/rewardedButton.ts, fuelGuard.ts, gold/goldAd.ts, topBar/bonusPlus.ts) |
placement, same names as ad |
fuel_empty |
the out-of-fuel dialog (ui/fuelGuard.ts) |
fuel ads left today |
The server drops unknown names, cuts a batch at 50 and clamps the phone’s clock to the last day. A player’s
day 0 is their first event, so “day since install” is worked out in the reports, not stored. A guest who
links an account takes its events along (db/linkGuest.ts). Logging never fails what it describes: a
race or purchase stays paid when its event cannot be written (logEvents in db/events.ts).
Turning it off
Section titled “Turning it off”One switch, Share data in Settings, covers these statistics and the crash reports
(core/crashReportSetting.ts). On by default (legitimate interest, privacy policy sections 1 and 9). Off
empties the game’s queue and sends { off: true } once, which sets profile.analytics_off; from then on
every server-side event for that player is skipped in the same SQL statement that would write it.
Deletion and export
Section titled “Deletion and export”Delete account and the yearly removal of idle guests keep the events but replace user_id with
deleted-<random uuid> (unlink in db/userData.ts), so retention and the ad gap keep their history
without naming anyone. My data includes the events. Events have no foreign key to user for that reason.
Reports
Section titled “Reports”npm run stats reads the live database (wrangler logged in), npm run stats -- --local the one
npm run dev uses. The queries live in scripts/statsQueries.ts:
- players, player days, sessions and empty tanks per player a day;
- retention D1, D7 and D30 (of the players whose day N has passed, the share that played on day N);
- rewarded ads by placement: offered, watched, watched per player a day;
- the share of players who watch rewarded ads at all;
- days to each boss’s first win, split by whether the player watched any rewarded ad before it: the measured side of Pace with and without ads;
- where players stop: players gone 7 days or more, by the last story beat they reached;
- cash and gold earned and spent, by source (race, each garage action, purchases);
- A/B groups: per group, players, D1 and D7 retention, rewarded ads per player a day and gold bought.
Every event row carries the player’s A/B group from remote config in ab_group
(migration 0018_event_group.sql): the variation the request that wrote it was served, default outside a
test, NULL before sign-in. The server fills it, so the game’s own events get it too. A player’s group in
the report is the one on their latest event; a player moved between groups counts in the newer one.
D1 bills rows written, not traffic. About 50 events a player a day (an estimate) fits the Workers Paid plan’s 50 million included writes a month up to about 30,000 daily players; past that, 1 USD per million rows (developers.cloudflare.com/d1/platform/pricing, read 2026-11-12). The free plan’s 100,000 writes a day hold about 2,000 daily players, enough for the beta.