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://indie.fun/js/indie.js"></script>
<script>
  new Indie({ 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 indie.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.
Retention (day 1 / 7 / 14 / 28), and its two readings
Every horizon takes the players whose first day was long enough ago to judge it, and asks whether they played again. "Day N" is the industry standard, the one game portals publish: it counts a player only if they played again on day N itself, so its percentages are small by nature. "Within N days" counts them if they came back at any point inside the horizon, which is more forgiving and more useful for a game people play in bursts. The two are the same number at day 1, which is why day 1 compares cleanly with figures from anywhere else. Each horizon has its own cohort, taken from the days it can already answer for, so the four cover different groups of players and a row can rise or fall across them.
Returning players
Of the players active on a given day, the share who had played on some earlier day. This is not retention and it is easy to confuse with it, because the two land in a similar range: retention follows one group of players forward from their first day, while this looks at one day and asks how much of the room came back. The headline number pools the days in your range, so it moves when you change the range, but the per-day figure does not depend on the range at all.
Conversion
The share of plays that lasted at least a minute. It is the one number that says whether people who open your game stay past the first moments: average session length hides that behind its long tail, and retention only speaks about people who came back on another day. Measured over plays whose length we recorded, because a play we never timed cannot be said to have converted or not.
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, five day-by-day charts (active players, day-1 retention, session length, returning players, conversion), the retention card 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 Indie({ appId: 'your-app-id', consent: 'auto' });   // default
new Indie({ appId: 'your-app-id', consent: 'manual' }); // your own UI
new Indie({ 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 indie.optIn() and indie.optOut(); read the current state with indie.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.

What a guest who hasn't chosen costs

Nothing. Until a visitor in the EU/UK opts in, the SDK stores nothing on their device and sends us no request at all, including the one that puts them in your game's live player count. They start counting the moment they choose Allow, and an opt-out takes them back out at once rather than at the end of some window.

Analytics only, no login UI

For a game embedded somewhere that has its own accounts, a portal like CrazyGames or Poki, or your own site with its own sign-in, everything on this page works with nobody signed in. Sessions, session length, frame rate, progression and crashes are all recorded against an anonymous device id.

const indie = new Indie({
  appId: 'your-app-id',
  login: false,        // no login pill; indie.login() still works if you want it
  consent: 'manual',   // no consent bar; you drive optIn()/optOut()
  // errors: false,    // optional: turn crash reporting off
  // performance: false, // optional: turn frame-rate measurement off
});

Bundling the SDK instead of loading the script tag, which most portal builds do, is the same config: import { IndieClient } from 'indie-sdk/browser', then new IndieClient({ ... }).

What that leaves running, measured: two POSTs when the visit opens, one for the session and one saying this tab is playing, then those same two once a minute, plus whatever your game reports with progress() and any crash. Nothing is read from the page around it and nothing is drawn on top of it.

'manual' moves the consent duty to you

The default, 'auto', is the SDK asking EU/UK visitors itself and waiting for an answer. Choosing 'manual' means your page, or the portal it sits in, is now the thing that has to ask, and nothing is counted for those visitors until you call indie.optIn(). That is a real obligation, not a default worth changing to keep a bar off the screen.

See Authentication for the login half: what login: false does and does not turn off, and how to open the popup from your own button.

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://indie.fun/api/sdk/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    appId: indie.getAppId(),
    sessionId: indie.getSessionId(),
    deviceId: indie.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.
Analytics · Indie SDK