SDK / Unreal

Analytics events

UnstableUnreal 5.8.4+

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.

In Unreal they are -brain and -brain-off.

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

#include "BrainSubsystem.h"

UBrainSubsystem* Brain = GetGameInstance()->GetSubsystem<UBrainSubsystem>();
Brain->RecordEvent(TEXT("level_end"),
    TEXT("{\"level\":\"forest_3\",\"won\":true,\"duration_ms\":84200,\"difficulty\":\"normal\"}"));

Fields cross as JSON object text, and Blueprint has the same RecordEvent node. Events go out only once you configure the subsystem with an IngestOrigin; without one they stay on the device.

FBrainOptions Options;
Options.Game = TEXT("my-game");
Options.PlayOrigin = TEXT("https://play.asebrain.com");
Options.IngestOrigin = TEXT("https://ingest.asebrain.com");
Options.PrivateRoot = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir() / TEXT("Brain"));
Brain->Configure(Options, FString());

Call Configure once, on the game thread. Packaged builds read the game's config from the build; in the editor, pass it as JSON in the second argument. bSendDebugBuilds makes non-Shipping builds report too.

Name screens

Name each screen as the player reaches it. The SDK records the time spent on the previous one.

Brain->SetScreen(TEXT("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 an empty string.

Brain->SetTelemetryContext(TEXT("place"), TEXT("match"));
Brain->SetTelemetryContext(TEXT("mode"), TEXT("duel"));

Loads and network attempts

Brain->RecordLoad(TEXT("level_3"), 1840);
Brain->RecordNetAttempt(TEXT("join_lobby"), false, 5000, TEXT("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->RecordNetSample(TEXT("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.

Brain->SetTelemetryOptedIn(bOn);
const bool bOptedIn = Brain->IsTelemetryOptedIn();

Brain->SuppressTelemetry(TEXT("autoplay"));

Unreal has no privacy URL helper yet. Link to /privacy on your Play origin with your game's id (https://play.asebrain.com/privacy?game=<id>), so the policy names your studio and its deletion and export links sign in with your game's keys.