> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/add-features/metrics.md).

# SDK 事件记录

整理 SDK 在没有用户代码的情况下自动收集并发送到平台的运行时事件。不是“我应该测量什么”，而是 **已经在测量什么**的文档。

### 如何开启

`Runtime/Helpers/AIT.PerformanceLogger.cs`的 `AITPerformanceLogger`为 `[RuntimeInitializeOnLoadMethod(BeforeSceneLoad)]`自动初始化。无需安装或调用。

发送会 **仅在 WebGL 构建中** 发生。在 Unity Editor 和其他平台上， `SendLog`会在进入时立即返回，因此事件既不会创建，也不会发送。因为桥接只存在于 WebGL 构建中。

所有事件的 `log_type`是 `unity_runtime`，事件种类则通过 `log_name`区分。

### 事件类别

| log\_name                 | 触发条件                                                              | 速率限制                     |
| ------------------------- | ----------------------------------------------------------------- | ------------------------ |
| `unity_scene_transition`  | `SceneManager.sceneLoaded` / `sceneUnloaded`                      | 无                        |
| `unity_first_interactive` | 原本第一个场景加载完成                                                       | 每个会话 1 次                 |
| `unity_low_memory`        | `Application.lowMemory`                                           | 每 30 秒 1 次               |
| `unity_error`             | `Application.logMessageReceived` (Error/Exception/Assert)         | 每 60 秒 10 次 + 去重         |
| `unity_lifecycle`         | `AITVisibilityHelper.OnVisibilityChanged`, `Application.quitting` | focus\_changed：每 5 秒 1 次 |
| `unity_frame_stall`       | `Time.unscaledDeltaTime` > 500ms                                  | 每 60 秒 5 次               |
| `unity_screen_change`     | `Screen.width`/`height`/`orientation` 检测到变化                       | 每 2 秒 1 次                |
| `unity_gc_collection`     | `GC.CollectionCount(0)` 检测到变化                                     | 每 60 秒 5 次               |
| `unity_timescale_change`  | `Time.timeScale` 检测到变化                                            | 每 5 秒 1 次                |

通过轮询检测的项（`frame_stall`, `screen_change`, `gc_collection`, `timescale_change`）是通过专用的 `AITPerformanceLoggerMonitor` GameObject 的 `Update`每帧检查一次。这个对象是 `HideAndDontSave` + `DontDestroyOnLoad`，因此在 Hierarchy 中不可见，并且会在场景切换中存活下来。

> **参考**：焦点事件来自 `Application.focusChanged`而不是 SDK 自身的 `AITVisibilityHelper`。因为在 WebGL 中，浏览器标签页可见性才是真正的信号。

### 按事件划分的参数

所有事件都包含以下通用参数。

| 参数                     | 说明                    |
| ---------------------- | --------------------- |
| `event_type`           | 同一 `log_name` 中区分细分类型 |
| `time_since_start_sec` | 应用启动后的经过时间（保留 1 位小数）  |

`unity_first_interactive`仅在 `time_since_start_sec` 改为 `time_since_start_ms`。

#### unity\_scene\_transition

```json
{
    "event_type": "scene_loaded",
    "scene_name": "GameScene",
    "scene_build_index": 2,
    "load_mode": "Single",
    "previous_scene": "MainMenu",
    "total_loaded_scenes": 3,
    "time_since_start_sec": 12.5
}
```

| 参数                    | 说明                                   | event\_type     |
| --------------------- | ------------------------------------ | --------------- |
| `event_type`          | `scene_loaded` 或 `scene_unloaded`    | 全部              |
| `scene_name`          | 场景名称                                 | 全部              |
| `scene_build_index`   | Build Settings 索引                    | 全部              |
| `load_mode`           | `Single` 或 `Additive`                | 仅 scene\_loaded |
| `previous_scene`      | 之前加载的场景名称                            | 仅 scene\_loaded |
| `total_loaded_scenes` | 当前已加载的场景数（`SceneManager.sceneCount`) | 全部              |

#### unity\_first\_interactive

这是一个衡量原本第一个场景加载结束、也就是游戏真正可以操作的那一刻的事件。每个会话只发送一次。

```json
{
    "event_type": "first_interactive",
    "scene_name": "MainMenu",
    "scene_build_index": 0,
    "time_since_start_ms": 4820
}
```

触发判定有两个规则。

* **`AITProxyBoot`开头的场景会跳过。** 因为 SDK 注入的代理启动场景并不是游戏原本的第一个场景。
* **无论是否处于激活状态，都会在最初的目标场景中确定“第一个”。** 即使关闭了日志记录，该场景中的标志也会被固定下来，因此之后加载的场景不会被迟到地报告为 first。

激活状态会在构建时把模板中写入的值通过 jslib 查询一次后缓存起来。如果查询失败， **视为已激活**。 (fail-open)

> **参考**：启动时第一个场景加载发生在 first-paint 之前，所以在没有额外优化的构建中，这个值会和 first-paint 时刻几乎相同。如果两个指标之间的间隔变大，那就是第一个场景变重了的信号。

#### unity\_low\_memory

```json
{
    "event_type": "low_memory",
    "time_since_start_sec": 120.5
}
```

#### unity\_error

```json
{
    "event_type": "exception",
    "message": "NullReferenceException: ...",
    "stack_trace": "at GameManager.Update() ...",
    "log_type": "Exception",
    "time_since_start_sec": 45.2
}
```

| 参数            | 说明                                               |
| ------------- | ------------------------------------------------ |
| `event_type`  | `error`, `exception`, `assert`                   |
| `消息`          | 错误消息（截断为 500 字）                                  |
| `stack_trace` | 堆栈跟踪（截断为 200 字）                                  |
| `log_type`    | Unity `LogType` (`Error`, `Exception`, `Assert`) |

去重是 **在 60 秒窗口内按消息哈希**。如果同一消息在窗口内重复出现，只发送第一条；窗口过去后哈希集合会被清空并再次报告。即使堆栈跟踪不同，只要消息相同就视为同一条。

#### unity\_lifecycle

```json
{ "event_type": "focus_changed", "has_focus": true, "time_since_start_sec": 120.5 }
{ "event_type": "quitting", "session_duration_sec": 300.5, "total_scenes_loaded": 5 }
```

`total_scenes_loaded`是会话期间加载的 **累计** 场景数， `unity_scene_transition`的 `total_loaded_scenes`与（当前同时加载数）不同。

#### unity\_frame\_stall

```json
{
    "event_type": "frame_stall",
    "frame_duration_ms": 750,
    "threshold_ms": 500,
    "time_since_start_sec": 45.2
}
```

判断标准是 `Time.deltaTime`而不是 `Time.unscaledDeltaTime`。 `Time.timeScale`设为 0 的暂停区间不会被算作卡顿。

#### unity\_screen\_change

```json
{ "event_type": "screen_resize", "width": 1920, "height": 1080, "previous_width": 1280, "previous_height": 720, "time_since_start_sec": 30.0 }
{ "event_type": "orientation_change", "width": 1080, "height": 1920, "orientation": "Portrait", "previous_orientation": "LandscapeLeft", "time_since_start_sec": 30.0 }
```

当尺寸和方向一起变化时， `orientation_change` 只会发送一个。因为旋转通常会伴随尺寸变化，所以避免两个事件重叠发送。

#### unity\_gc\_collection

```json
{
    "event_type": "gc_collection",
    "generation": 1,
    "gen0_total": 45,
    "gen1_total": 12,
    "gen2_total": 3,
    "time_since_start_sec": 60.0
}
```

检测 **只看 gen0 计数变化** 。 `generation` 值是根据 gen1/gen2 累计计数推算出来的，因此并不能准确指出这次回收到底属于哪一代。细 `gen*_total`是进程启动以来的累计值，所以这些值更可靠。

#### unity\_timescale\_change

```json
{
    "event_type": "timescale_changed",
    "time_scale": 0.0,
    "previous_time_scale": 1.0,
    "time_since_start_sec": 15.0
}
```

### 在调试控制台中查看

打开调试控制台后，可以通过屏幕左下角按钮打开控制台， **指标** 选项卡直接看到这些事件。会显示事件列表和按类别的累计计数，因此无需查看平台仪表板也能立即确认是否在正常采集。

调试控制台在 Dev Server 配置文件中默认启用，在其他配置文件中也可以 [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles)的 `AIT_DEBUG_CONSOLE` 通过环境变量开启。

> **参考**：按类别的计数表通过匹配上面 8 个类别名称的子字符串来工作。 `unity_first_interactive`不会匹配到其中任何一项，因此会在表格下方作为单独一行显示。这不是遗漏，而是分类方式不同。

### 安全措施

| 项目        | 说明                                                                                   |
| --------- | ------------------------------------------------------------------------------------ |
| try-catch | 包裹所有处理器，避免日志记录失败导致游戏停止                                                               |
| 防止重入      | `_isSending` 通过 `logMessageReceived` → `SendLog` → 警告日志 → `logMessageReceived` 阻止死循环 |
| 速率限制      | 通过按类别设置固定上限来防止过量发送                                                                   |
| 字符串截断     | 在固定长度处截断错误消息和堆栈跟踪                                                                    |

重入保护尤其重要。 `SendLog`因为在 WebGL 以外的环境中 `Debug.LogWarning`不会使用，因此如果留下警告，该警告会再次通过 `logMessageReceived`进入。

### 相关文档

* [构建配置文件](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — 开关调试控制台
* [Sentry集成](https://developers-apps-in-toss.toss.im/documentation/unity/add-features/sentry-integration) — 也把错误发送到 Sentry
* [API 使用模式](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/api-usage-patterns) — 直接发送事件的 Analytics API


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/api-and-sdk-zh/unity/add-features/metrics.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
