Analytics

Daily active players, average session length, and retention, measured from real sessions, including players who never sign in.

Set it up

There is no analytics setup. Creating the browser client starts it:

<script src="https://strafe.fun/js/strafe.js"></script>
<script>
  new Strafe({ appId: 'your-app-id' });
</script>

From that one line the SDK:

  • opens a session when the page loads, or carries on the one this player was already in,
  • reports how long it has run every 60 seconds,
  • flushes one last duration as the tab closes, without it every session would round down to a whole number of minutes, and anything shorter than a minute would read as zero,
  • measures the frame rate it managed while the game was being played, and any stall of a quarter-second or more,
  • identifies the player by their account when they're signed in, and by a persistent first-party device id when they're not.
You do not need login for analytics. An anonymous player counts toward DAU, session length and retention exactly like a signed-in one, and when they later sign in on the same browser, their anonymous history folds into that account instead of counting twice.

What each number means

Session
One play, one visit, not one page load. A reload carries on the same session, and so does a link out and back: what starts a new one is thirty minutes with this player away. Its length is time spent playing, not time the tab was open: the clock stops while the tab is in the background and while nobody is touching the game, and starts again when they come back. Capped at 24 hours. A session whose length we never learned is still counted as a play, but stays out of the duration average rather than entering it as a zero.
The thirty minutes
What separates one visit from the next. A game that reloads between rounds, a player who refreshes after a crash, a quick trip to a wiki and back, all one session, because none of them are the player leaving. It is not the idle rule: nothing accrues while they are away, so a resumed session gains that gap's history but none of its time. Two tabs of the same game are still two sessions; only the tab that opened the visit continues it.
Player
An account if there is one, otherwise the device. A device that has only ever been used by one account is folded into it, so a player who plays anonymously and then signs in is one player, not two. A shared browser seen with two accounts stays unlinked, attributing it would be a guess.
DAU (daily active players)
Players with at least one session that day. Days are UTC, everywhere, so the same number means the same thing regardless of where your players are.
Average session length
The mean over sessions whose length we actually measured. Both the KPI tile and the day-by-day chart use that same denominator, so they can never disagree.
Active time
What a session's length is made of. Time counts while your page is visible and the player is still doing something, a click, a key, a touch, a stick on a gamepad. Switching tabs stops it immediately; five quiet minutes stops it too, so a cutscene or a long think still counts but a game left running on a second monitor does not. Coming back starts it again in the same session. A reload does not reset any of this: the clock carries on where the previous page left off, idle counter included, so a game that reloads itself cannot keep an empty tab looking busy.
A page nobody touched
Not counted at all, not as a play, not in the average. Until someone clicks, presses, touches or comes back to the tab, we have no evidence a person is there, and a link unfurled by Discord, a crawler, a prefetch and an abandoned tab all load your page exactly like a player would. It is not a minimum: a player who leaves after ten seconds of actually playing is a play, measured at ten seconds, because a game people quit immediately is the most useful thing we can tell you. And the time before that first input is kept, so a cutscene watched and then clicked through counts in full.
Frame rate and stalls
Measured while the game is being played, on the same clock as active time, so a loading screen or a menu is not averaged into it. The SDK counts frames in a requestAnimationFrame callback, buckets the rate in tens, and records any frame gap of a quarter-second or more as a stall. A gap over ten seconds is not recorded at all: past that we cannot tell a freeze from the page simply not running. It rides on the heartbeat already being sent and costs nothing measurable (indistinguishable on a 60fps canvas game). Pass performance: false to turn it off. Nothing about a player is recorded, only how their machine coped, see the "How the game ran" card on your Dashboard tab.
How long your game takes to become playable
Not a separate metric, a progression step. Call strafe.progress('game_start') the moment a player can actually play and make it the first step of your funnel: the median time under it is your time-to-playable, and the drop before the next step is everyone who opened the game and left while it was still getting ready. Without that step your funnel begins wherever your first milestone is, and every player you lost before it is missing from the report rather than counted as a loss.
Day-1 retention
Of the players who played for the first time on a given day, the share that came back the next day.
The retention curve (day 1 / 7 / 14 / 28)
Of the players whose first day was long enough ago to judge, the share that came back at least once within that many days. All four numbers describe the same players, the ones old enough for the longest horizon shown, so the curve can only rise: everyone counted at day 7 is still counted at day 14. It deliberately ignores the date-range filter, because a 14-day view must not erase your day-28 number, which by definition needs older players. That also means it moves slowly and says nothing about this week: the day-1 chart above is where a recent change shows up.
Plays
Sessions somebody was actually in. The “N plays” on a game card, the all-time session count on your dashboard, and DAU are all counted from the same log with the same rule, so they can never tell you different stories, including that they all drop the loads where nobody was ever there.

A cohort needs a finished day to land on

Day-1 retention for yesterday's new players would be judged on today, which is still running, everyone who hasn't come back yet would score as churned. So the two most recent days read pending rather than zero, and fill in once the day is over.

Where to read them

The Dashboard tab on your game
KPI tiles, three day-by-day charts (active players, day-1 retention, session length) and the daily table. Pick a window of 7, 14, 30 or 90 days, and switch between all players and signed-in accounts only. This view counts today, so it moves as the day goes on.
The corner of every game card
The same three metrics at a glance, over whole days: the seven ending yesterday, against the seven before. A card must not count today, or every card in your grid would read as falling by mid-morning. Same definitions as the panel, different window, each tooltip says which days it covers.
The API
GET /api/me/creations/[gameId]/analytics?days=14&population=all, with your portal session. Returns dense daily buckets, every day in the window, including the empty ones.

Anonymous players and consent

Counting anonymous players means storing a device id in their browser, so the SDK asks or tells them itself. An embedding game can't be relied on to mention it in its own privacy policy. Set the mode when you create the client:

new Strafe({ appId: 'your-app-id', consent: 'auto' });   // default
new Strafe({ appId: 'your-app-id', consent: 'manual' }); // your own UI
new Strafe({ appId: 'your-app-id', consent: 'off' });    // you handle compliance
'auto' (default)
A small bar appears once, and is remembered. In the EU/UK it asks permission and nobody is counted until they choose Allow. Elsewhere it is a notice. It states what happens, offers Decline, and dismisses itself.
'manual'
No bar. Drive it from your own consent UI with strafe.optIn() and strafe.optOut(); read the current state with strafe.getConsent() ('granted' | 'denied' | 'unknown'). Guests in the EU/UK aren't counted until they opt in.
'off'
No region gate and no bar. Your site takes on compliance itself. Opt-outs and Do-Not-Track are still honoured.
Do Not Track and Global Privacy Control always count as a decline. Signed-in players are always counted, because they have an account with us. Declining stops guest tracking and drops the stored device id; what was already recorded stays.

Check it worked

Check it worked

1. Open the sandbox with your App ID. Session recorded should turn green within a second or two; leave the tab open a minute and the duration starts climbing, which is the heartbeat working.

2. From your own game, ask the same question of the session in front of you:

await fetch('https://strafe.fun/api/sdk/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    appId: strafe.getAppId(),
    sessionId: strafe.getSessionId(),
    deviceId: strafe.getDeviceId(),
  }),
}).then(r => r.json());

3. Open your game's Dashboard tab. Your visit shows up the same UTC day; day-1 retention for it stays pending until that day is over, which is correct, not a bug.

If nothing arrives

session: null from the check above means we never received it. In order of likelihood: the App ID is wrong or its keys were deleted; the player declined analytics (getConsent() returns 'denied', or Do-Not-Track is on, and getSessionId() is null as a result); or the request never left the browser. Check the console and your Content-Security-Policy.