本页尚未翻译,当前显示英文版本。
Analytics
所有需要事后统计、而不是当下查看的东西。 声明一个有类型的遥测事件并发出,它就会和平台自己的事件落在一起:关卡完成、漏斗步骤、经济事件、会话时长、教程中的流失。这个模块只负责发出;它自己不读取、不聚合,也不投递任何东西。批量数据发往哪里由路由器决定,见 Extensibility。
何时使用
- 某件事需要事后统计:漏斗(funnel)步骤、关卡完成、经济事件、会话时长。
- 对比必须跨越多个游戏 build 依然成立:类型带有 schema 版本,所以一年前的漏斗不会悄悄把同一字段的两种含义混在一起。
- 数据量很大,而且只要你事先声明过,丢一行也可以接受:遥测是契约中唯一允许声明式丢失的地方。
- 无需使用:如果有人需要做出反应。遥测事件根本没有订阅者;需要让别人知道的事实,应该用游戏事件。
分工
| 主体 | 可以 | 不可以 |
|---|---|---|
任意主体 | 在 schema 中声明类型;以自己的身份发出事件,逐条或批量(batch);读取已声明的类型 | 填写上下文;读取、查询或聚合已发出的内容 |
backend-service | 同上,另外可以通过委托代表玩家发出 | 读取遥测:没有读取权限,因为根本没有读取操作 |
概览
声明并发出
BossDefeated:用具名、有类型的字段代替 JSON 数据块[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))即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
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 });定义就是 schema,所以到达的数据带有具名、有类型的字段,而不是一团 JSON。它作为独立的种类声明,而不是带标志的游戏事件。
模型
遥测事件类型声明什么。
| 声明项 | 含义 |
|---|---|
name | 类型自己的名字 |
fields | 按平台的类型系统定类型;和其他地方一样,可以对它们应用字段掩码 |
schema 版本 | 必填,而且不从 SDK 版本推导:漏斗要比较在不同游戏 build 下收集的事件,没有版本,比较就会悄悄混入不可比的数据 |
sampling | 这个类 型的事件有多大比例能通过。在类型上声明,绝不由实现根据负载自行选择:比例如果自己变了,不同日期的漏斗就无法比较 |
丢失容忍度 | 这个类型是否容忍丢失 |
删除行为 | 玩家删除如何作用于这个类型:删除或匿名化。策略由工作室声明,模块负责执行 |
每个遥测事件始终成立的事实。
| 始终成立 | 含义 |
|---|---|
没有寻址目标 | 没有接收者、没有群组、没有订阅。想要指定接收方,就说明你需要的是游戏事件 |
上下文 | 由平台填写:它会加上主体或匿名标记、会话、环境、build 版本,以及按声明时钟记录的时刻。调用方无法填写:调用方即使替换了主体或 build 版本,得到的也是平台推导出的值,而不是传入的值 |
采样比例 | 随事件一起传递:没有它,就无法从到达的数据还原绝对数量 |
丢失 | 可以在汇总层面观测:一段时间内未送达的比例对消费方可见;不承诺逐条可观测,因为按遥测的数据量,每丢一条就发一条消息,本身就会变成一个流 |
发出 | 不是幂等的:两次调用就是两个事实,而且不接受 幂等键(idempotency key),因为压制第二次就会丢数据。一次丢失的发出无法确认结果,也不需要确认:丢失容忍度是事先声明的,对所有调用一次生效 |
事件 | 就是历史:模块自己不保存任何历史 |
错误
- 未声明的类型,以及与类型 schema 不匹配的字段,都属于校验拒绝;两者都不会在运行时让人意外,因为类型是从声明进入管理控制台的。
- 超大事件属于校验拒绝;平台从不悄悄截断字段。
- 超出频率时返回限流类别的错误,并附带一个时间点,在此之前重试没有意义。
- 采样丢弃不是错误,可接受的丢失也不是。 调用已经执行,丢弃是声明过的行为;把任何一种报告为失败,都会让声明的行为与故障无法区分。
- 批量要么整体生效,要么逐项生效,具体是哪种需要声明,绝不是“发生了什么算什么”。
限制
具体数值会随平台限制一章给出;触及每个上限时的行为现在就已确定。
- 每个主体的事件频率:以限流拒绝,并给出截止时间。超出频率绝不会悄悄丢弃:要么是这种拒绝,要么是声明过的采样丢弃。
- 单个事件的大小:校验拒绝,绝不悄悄截断字段。
- 批量大小:在发送前拒绝,而不是部分生效。
- 每个项目声明的类型数:部署时拒绝新的声明,而不是在运行时。
- 单个类型的字段数:同上,在部署时拒绝。