Skip to content

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.

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).

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.

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.

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.