Analytics events
Brain's analytics come in two parts: core events the SDK records on its own, and your game's own
events, which you describe in brain/events.toml and record from code.
What the SDK records on its own
Every game with the SDK reports these without any code:
| Event | When | What |
|---|---|---|
session_start |
Launch | OS, device, CPU, GPU, renderer, memory, screen, refresh rate, window mode, vsync, locale, input device |
presence |
Every 30 s | Scene, your context, whether the player is active, idle time, input device |
perf |
Every 10 s | FPS, frame time percentiles, hitches, draw calls, memory |
net_perf |
Every 10 s while online | Round trip, jitter, loss, traffic, your mispredictions and late inputs |
error |
As logged | Engine and script errors, with the stack when the engine has one |
screen |
When the player leaves a screen you named | The screen and the time spent on it |
session_end |
Quit | Session length and why it ended |
purchase |
Offer, buy and its end | The purchase funnel |
Matches, levels and rounds aren't core events, because not every game has them. Record them as your own.
When the SDK stays silent
So that test runs never mix with players' data, the SDK records nothing:
- When the player turned analytics off, or your game called suppress for this session.
- In debug or development builds, unless you turn on reporting from them.
- In builds without a version, such as editor runs. Brain's release builds stamp the version.
- In headless runs, other than your dedicated servers.
- In runs an AI coding agent starts.
- In store capture runs.
Launch flags override this for testing. --brain makes a run report anyway (but never against the
player's choice or a suppress call), and --brain-off silences it.
Pass them after --.
Describe your events
Each of your events has a spec in brain/events.toml:
[[event]]
type = "level_end"
table = "level_ends"
description = "Once per player when they finish or fail a level."
fields.level = { kind = "string", max = 64, description = "The level's id." }
fields.won = { kind = "bool", description = "Whether this player cleared the level." }
fields.duration_ms = { kind = "int", optional = true, unit = "ms", description = "Time from the level's start to its end." }
fields.difficulty = { kind = "enum", values = ["easy", "normal", "hard"], description = "The difficulty the player chose." }
| Field kind | Takes |
|---|---|
string |
Text, at most max characters |
int |
Whole numbers, optionally within min and max |
number |
Decimal numbers |
bool |
true or false |
enum |
One of values; any other value drops the whole event |
json |
A JSON value, at most max_bytes |
Mark a field optional = true when an event may leave it out.
Give every event and field a description: what one row stands for, and whose point of view a value
is (is won per player or per team?). Give every number a unit: s, ms, count, percent,
bytes, kb, mb, fps, hz or px. People on your team, and Claude when they ask it about
your game, read both to understand the data.
Your events can't reuse a core event's type or table.
Check and publish
Brain drops events whose fields don't match their spec, so check the file after every change:
ase games check --project .
Each release publishes brain/events.toml before any build ships, so an
event's spec is live by the time players send it.
Record an event
Brain.record("level_end", {
"level": "forest_3",
"won": true,
"duration_ms": 84200,
"difficulty": "normal",
})
fields is a Dictionary. The call returns at once; the SDK queues the event on disk and sends it
in batches.
Name screens
Name each screen as the player reaches it. The SDK records the time spent on the previous one.
Brain.screen("shop")
Say what the player is doing
Context travels with presence, perf and net_perf, so you can split performance and activity
by where players are. The keys are place, mode, level and players.
Clear a key with null.
Brain.set_context({"place": "match", "mode": "duel", "level": "arena", "players": 2})
Brain.set_context({"place": "menu", "mode": null, "players": null})
Loads and network attempts
Brain.loaded("level_3", 1840)
Brain.net_attempt("join_lobby", false, 5000, "timeout")
Network quality on other transports
The SDK reads network quality on its own from Godot's ENetMultiplayerPeer and Unreal's net
drivers. On another transport, or to add your netcode's own counters (mispredictions,
late_inputs), report them yourself.
Brain.net_sample({"role": "client", "transport": "steam", "rtt_ms": 42, "mispredictions": 3})
The player's choice
Every game's settings screen has an analytics toggle and a link to the privacy policy. Analytics are on until the player turns them off; off stops recording and sending at once. The choice is kept per device.
For runs that aren't a person playing (recordings, bots, autoplay), suppress the session at startup instead. That silences this run only and leaves the player's choice alone.
analytics_toggle.button_pressed = Brain.is_opted_in()
analytics_toggle.toggled.connect(Brain.set_opted_in)
privacy_button.pressed.connect(func() -> void: OS.shell_open(Brain.privacy_url()))
Brain.suppress("autoplay")
Brain.status() answers the install id and the queued and pending counts, for a debug overlay.