# Analytics for a game

> Online page: https://cassette.gg/agents/analytics.md. Fetch it again before you act on it; don't work from a saved copy.
> Start of every task: https://cassette.gg/agents/guide.md.

Every game on Cassette gets analytics for free, with nothing to add to the game. The human sees them in the dashboard
(https://cassette.gg/dashboard, the Analytics tab); you read the same numbers from the API, and you add your own events to the game
with one call. Only aggregates are ever returned: how many plays and players, never which ones.

Your key needs `dev:read` to read, and `dev:publish` to save or delete a funnel. `$API` is `https://cassette.gg/v1`.

## 1. What is counted without any code

| Number | Meaning |
|---|---|
| Plays | A play session that started, counted for the day it began. One player counts at most 5 plays a day, in the totals, the daily series, every breakdown row and the length buckets alike, so a row is never more than all the plays. |
| Players | Distinct players: a signed-in player is one account; a guest is one connection, so a guest who changes network counts twice. |
| New / returning | New: the player's first day on this game. |
| Time per play | How long the game was in front of the player: the time the tab was visible, not the time it stayed open (never more than the time Cassette heard from the game, at most 4 hours). Also the median, and a histogram. |
| Store visits | Page loads of the game's store page (`/g/<slug>`) or its play page (`/play/<slug>`): arriving from a link, an ad, a search or a bookmark. Crawlers and prefetches are not counted, and neither is moving between pages inside Cassette, so a campaign's visits are its arrivals. |
| Retention | Of the new players whose day 1, 7 or 30 fell in the period you are looking at, how many played again that day (so a week has its day 7 and a month its day 30). A figure appears once the day has passed and at least 5 players are behind it. A play that runs past midnight is not a return the next day, and a guest who signs in halfway through a play is still one player. |
| By | day, device (desktop, phone, tablet), country, traffic source (paid ads, social, search, email, other sites, the Cassette store, a direct link), source name and campaign. |
| Purchases, revenue | From sales, net of refunds: per day and per campaign. |

A breakdown row (a country, a device, a source, a campaign, an event property's value) is shown only if at least **5
distinct players** are behind it; smaller rows are folded into "Other", and "Other" is shown only if it passes the same
bar. A row that only has store visits (a campaign nobody has played from yet) needs 5 visits. Totals and the daily series
are always shown. Updated every few minutes.

## 2. Read the numbers

`GET $API/dev/games/{game}/analytics?range=7d|30d|90d` (default `30d`) returns:

- `totals` and `previous` (the same length of time just before): `plays`, `players`, `newPlayers`, `returningPlayers`,
  `avgSessionSec`, `views`, `purchases`, `revenue`.
- `series` and `previousSeries`: a row per day with the same fields.
- `retention`: `d1`, `d7`, `d30` as `{ rate, cohort }` (`rate` is `null` while there are fewer than 5 players), and a
  `series` of cohorts by week of first play, reaching back at least 60 days so a day 30 can be drawn.
- `sessionLength`: `medianSec` and `buckets` (`<30s` … `30m+`) with plays.
- `dims.country | device | channel | source | campaign`: `rows` of `{ key, views, plays, players, newPlayers,
  avgSessionSec, share }`, and `purchases`, `netCents` on `channel`, `source` and `campaign`.

`{game}` is the game's id or slug. The older `GET $API/dev/games/{game}/stats` still answers, with fewer fields.

## 3. Your own events: `cassette.track`

In the game, with the runtime script (https://cassette.gg/agents/runtime-sdk.md):

```js
Cassette.track('level_complete', { level: 3, mode: 'hard' })
```

- A third-party tag (Google Analytics and the like) loads in the game's frame but has no cookies or storage there, so it can't tell one visit from the next or
  keep a consent choice. Use `Cassette.track` for what you want counted; Cassette already counts the plays and players.
- A name is 1 to 40 characters of `a-z 0-9 _ . : -`, starting with a letter (upper case is lowered). `@play` is reserved.
- At most 8 properties. A key is `a-z 0-9 _`, up to 24 characters. A value is a string (cut at 48 characters), a finite
  number or a boolean; anything else is dropped. A string that looks like an email address is dropped. All of it fits in
  400 bytes.
- Never put a player's name, email or any identifier in an event. Events are for what happened, not who did it.
- Limits Cassette enforces itself, whatever the game sends: 1,000 events per play, 2,000 per player per game per day,
  20,000 per game per day, 100 different names per game, and at most 10 new
  names from one play and 20 a day from one player (anyone can play any game, so nobody can fill another game's list in
  one go). `track` never throws and never blocks the game.

Only the Cassette player starts a session and holds its token, so a game cannot send events for another game, and a
page outside Cassette cannot send any.

`GET $API/dev/games/{game}/events?range=` lists each event with `count`, `sessions` and a daily `series`, what was
refused and why (`refused`) and the `limits`. **An event is listed only once at least 5 different players have sent it**
(a name is text the game chooses, and with fewer players it could be one player's identity); `fewPlayers` says how many
names are held back. `GET $API/dev/games/{game}/events/{name}?range=7d|30d` gives one event's days, the values at least 5
players used for each property (the top 10), and for a numeric property its average and median, and from 20 players the
10th and 90th percentile; never its lowest or highest value, which would be one player's.
`DELETE $API/dev/games/{game}/events/{name}` (`dev:publish`) takes a name out of the game's list, with its totals and
stored events: use it for a name a script filled the list with, or one the game no longer sends.

## 4. Funnels

A funnel counts the plays that sent a list of events in order: 2 to 6 steps, each a custom event name or `@play` (a
play that started). At most 5 per game. A step on an event that fewer than 5 players sent has no number
(`sessions: null`, `fewPlayers: true`).

- `GET $API/dev/games/{game}/funnels?range=` : each funnel with the plays that reached every step.
- `POST $API/dev/games/{game}/funnels` `{ "name": "First levels", "steps": ["@play", "level_1", "level_2"] }`.
- `DELETE $API/dev/games/{game}/funnels/{id}`.

## 5. Ads: where players come from

A visit's campaign comes from the link it landed on: `utm_source`, `utm_medium`, `utm_campaign` (and `utm_term`,
`utm_content`), and the click id an ad network adds (Reddit's `rdt_cid`, Google's `gclid`, `gbraid`, `wbraid`). The
first landing of the visitor's tab decides, and it follows the visitor to the play and to a purchase. The click id only
tells Cassette which network sent the visit; it is not shown to you.

Make a link: `GET $API/dev/games/{game}/tracked-link?source=reddit&medium=paid&campaign=launch-week&to=g` returns
`{ "url": "https://cassette.gg/g/<slug>?utm_source=reddit&utm_medium=paid&utm_campaign=launch-week" }` (`to=play` lands on the
game itself). Nothing is stored; a link written by hand with the same parameters counts the same. The analytics then
show, per campaign: store visits, plays, visit-to-play rate, purchases and revenue.

Cassette does not send conversions to the developer's own Reddit or Google Ads account yet. The numbers above are the
way to measure an ad today: compare the campaign's spend in the ad account with its plays, purchases and revenue here.

## 6. What to tell the human

- Analytics need no setup. The first plays show up within minutes.
- Events are for questions the human wants answered ("how many finish level 3?"): ask which, then add the matching
  `track` calls.
- Say what is and isn't counted: guests are counted by connection, and small groups are hidden to protect players.

---

Code in this document is MIT-licensed: `SPDX-License-Identifier: MIT`, Copyright (c) 2026 Delegas Labs, S.L. Copy and
adapt it freely.
