Skip to main content
Services around the game

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

ActorCanCannot
any actordeclare types in schema; emit on its own behalf, one at a time or as a batch; read the declared typesfill in the context; read, query or aggregate what was emitted
backend-servicethe same, and emit on behalf of a player by delegationread telemetry — there is no read permission, because there is no read operation

At a glance

Declaring and emitting 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))
Coming soon — Go

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.
Unity C# is the same C# API here — the same declaration and Emit call compile in Unity, on the 2021.3 baseline
[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.

DeclaresWhat it is
namethe type's own
fieldstyped by the platform's type system; a field mask applies to them as everywhere else
schema versionmandatory, 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
samplingwhat 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 tolerancewhether this type tolerates loss
deletion behaviourhow 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.

AlwaysWhat it is
no addressing targetno recipient, no group, no subscription. Wanting to address one is the sign that a game event is what is needed
contextis 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 sharetravels with the event: without it the absolute number cannot be reconstructed from what arrived
lossis 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
emissionis 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 eventsare 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.