Daily active players, average session length, and retention, measured from real sessions, including players who never sign in.
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:
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.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.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.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 complianceindie.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.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.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
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.