Not translated yet — showing English.
Analytics
Everything that needs to be counted later rather than seen now. Declare a typed telemetry event, emit it, and it lands beside the platform's own — a level completed, a funnel step, an economic event, session length, a drop-off in the tutorial. This module emits; it does not read, aggregate or ship anything itself. Where a batch travels is the router's decision, in Extensibility.
When to use it
- Something must be counted later: a funnel step, a level completed, an economic event, session length.
- The comparison has to survive game builds: a type carries a schema version, so a year-old funnel doesn't silently mix two meanings of one field.
- The volume is high and a lost row is acceptable if you said so: telemetry is the only place in the contract where declared loss is lawful.
- Skip it when someone has to react. A telemetry event has no subscribers at all; a fact others must hear is a game event.
Who does what
| Actor | Can | Cannot |
|---|---|---|
any actor | declare types in schema; emit on its own behalf, one at a time or as a batch; read the declared types | fill in the context; read, query or aggregate what was emitted |
backend-service | the same, and emit on behalf of a player by delegation | read telemetry — there is no read permission, because there is no read operation |
At a glance
BossDefeated: named, typed fields instead of a JSON blob[Event("boss_defeated")]
public class BossDefeated
{
public string BossId = "";
public int PartySize;
public float FightSeconds;
}
PlayServ.Analytics.Emit(new BossDefeated { BossId = "hydra", PartySize = 4, FightSeconds = 212f });@Event('boss_defeated')
export class BossDefeated {
bossId = '';
partySize = 0;
fightSeconds = 0;
}
PlayServ.analytics.emit(new BossDefeated({ bossId: 'hydra', partySize: 4, fightSeconds: 212 }));@event("boss_defeated")
class BossDefeated:
boss_id: str = ""
party_size: int = 0
fight_seconds: float = 0.0
playserv.analytics.emit(BossDefeated(boss_id="hydra", party_size=4, fight_seconds=212.0))Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
USTRUCT(PSEvent = (Name = "boss_defeated"))
struct FBossDefeated
{
GENERATED_BODY()
UPROPERTY() FString BossId;
UPROPERTY() int32 PartySize;
UPROPERTY() float FightSeconds;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
// the declared type becomes a generated member under the Emit node
Client->Analytics->Emit->BossDefeated({ TEXT("hydra"), /*PartySize*/ 4, /*FightSeconds*/ 212.f });
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.[Event("boss_defeated")]
public class BossDefeated
{
public string BossId = "";
public int PartySize;
public float FightSeconds;
}
PlayServ.Analytics.Emit(new BossDefeated { BossId = "hydra", PartySize = 4, FightSeconds = 212f });The definition is schema, so what arrives carries named, typed fields rather than a JSON blob. It is declared as its own kind, not as a game event with a flag.
The model
What a telemetry event type declares.
| Declares | What it is |
|---|---|
name | the type's own |
fields | typed by the platform's type system; a field mask applies to them as everywhere else |
schema version | mandatory, and not derived from the SDK version — a funnel compares events collected under different game builds, and without a version the comparison silently mixes the incomparable |
sampling | what share of events of this type gets through. Declared on the type, never chosen by the implementation according to load: a share that changed by itself would make funnels incomparable between days |
loss tolerance | whether this type tolerates loss |
deletion behaviour | how a player's deletion reaches this type — by deleting or by anonymising. The studio declares the policy; the module carries it out |
What is true of every telemetry event.
| Always | What it is |
|---|---|
no addressing target | no recipient, no group, no subscription. Wanting to address one is the sign that a game event is what is needed |
context | is the platform's: it adds the actor or a marker of anonymity, the session, the environment, the build version, and the moment by the declared clock. The caller cannot fill it in: a caller that substitutes the actor or the build version gets the derived values instead of the passed ones |
the sampling share | travels with the event: without it the absolute number cannot be reconstructed from what arrived |
loss | is observable in aggregate: the share undelivered over a period is available to the consumer; per-item observability is not promised, because at telemetry volumes a message per loss would itself become a stream |
emission | is not idempotent: two calls are two facts, and it accepts no idempotency key — suppressing the second would lose data. There is no way to establish the outcome of a lost emit, and none is wanted: loss tolerance is declared in advance, for every call at once |
the events | are the history: the module keeps none of its own |
Errors
- An undeclared type, and a field that does not match the type's schema, are validation refusals; neither is a runtime surprise, because a type reaches the admin console from its declaration.
- An oversized event is a validation refusal; fields are never clamped silently.
- The rate exceeded answers in the rate-limit category, carrying the time before which a retry is pointless.
- A sampling drop is not an error, and neither is an admissible loss. The call was executed and the drop is declared behaviour; reporting either as a failure would make declared behaviour indistinguishable from a fault.
- A batch is whole or per element, and which one is declared — never "whatever happened".
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- Event rate per actor: a rate-limit refusal with a deadline. Exceeding the rate never drops silently: it is either that refusal or a declared sampling drop.
- Size of one event: a validation refusal, never silently clamped fields.
- Batch size: rejected before sending rather than applied partially.
- Declared types per project: a new declaration is rejected at deploy time, not at runtime.
- Fields in a type: the same, at deploy time.
- Retention period: past the declared period the event is no longer available.