跳到主要内容
游戏周边服务

本页尚未翻译,当前显示英文版本。

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.
Unity C# is the same C# API here — 同样的声明和 Emit 调用可以在 Unity 中编译,基线为 2021.3
[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 不匹配的字段,都属于校验拒绝;两者都不会在运行时让人意外,因为类型是从声明进入管理控制台的。
  • 超大事件属于校验拒绝;平台从不悄悄截断字段。
  • 超出频率时返回限流类别的错误,并附带一个时间点,在此之前重试没有意义。
  • 采样丢弃不是错误,可接受的丢失也不是。 调用已经执行,丢弃是声明过的行为;把任何一种报告为失败,都会让声明的行为与故障无法区分。
  • 批量要么整体生效,要么逐项生效,具体是哪种需要声明,绝不是“发生了什么算什么”。

限制

具体数值会随平台限制一章给出;触及每个上限时的行为现在就已确定。

  • 每个主体的事件频率:以限流拒绝,并给出截止时间。超出频率绝不会悄悄丢弃:要么是这种拒绝,要么是声明过的采样丢弃。
  • 单个事件的大小:校验拒绝,绝不悄悄截断字段。
  • 批量大小:在发送前拒绝,而不是部分生效。
  • 每个项目声明的类型数:部署时拒绝新的声明,而不是在运行时。
  • 单个类型的字段数:同上,在部署时拒绝。
  • 保留期:超过声明的期限后,事件不再可用。