@video-lab/telemetry 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -42
- package/dist/index.cjs +64 -17
- package/dist/index.d.cts +17 -6
- package/dist/index.d.mts +17 -6
- package/dist/index.mjs +64 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @video-lab/telemetry
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
宿主应用的播放器会话遥测核心。它把五种接入方式交付的 `PlayerEvent` 归一为安全、可关联的 record,并在会话结束时生成 QoE summary。它只依赖 [`@video-lab/protocol`](https://www.npmjs.com/package/@video-lab/protocol),不依赖播放器内核、React、Vue、Sentry 或 HTTP 客户端。
|
|
4
|
+
|
|
5
|
+
它解决“播放器发生了什么、用户体验如何、是否恢复、发生在哪个宿主版本”的本地事实归一;它不解决网络上传、鉴权、采样、队列、重试或告警平台初始化。这些仍由宿主应用的传输层负责。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -10,17 +10,39 @@
|
|
|
10
10
|
pnpm add @video-lab/telemetry
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## 上报范围:四类事实,一层共同归因
|
|
14
|
+
|
|
15
|
+
| 维度 | 回答的问题 | 包输出的事实 |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Error | 什么失败了、能否重试? | 受控 `error` 事件、错误码、脱敏播放上下文;summary 的 `qoe.errors`。当前是 **19 个 `E_*` 错误码**。 |
|
|
18
|
+
| QoE | 用户是否顺畅观看? | 首帧、播放期卡顿、质量切换、换源、内核/音频/缓冲健康、PC 帧冻结,以及对应的 `unknown` coverage。 |
|
|
19
|
+
| Behavior | 用户或宿主做了什么? | SDK 控件产生的 `useraction` 原始事件;宿主显式写入的受限 `intent`。 |
|
|
20
|
+
| Recovery | 自动恢复有没有真正恢复观看? | `recovery` 生命周期、尝试/终态和“播放且位置推进”的恢复证据。 |
|
|
21
|
+
| 共同归因 | 发生在哪里、数据是否可信? | `application`、脱敏 `PlaybackContext`、`sessionId`、`eventId`、`sequence`、delivery 证据与 `completeness`。 |
|
|
22
|
+
|
|
23
|
+
另有 **3 个 `W_*` 警告码**。错误码与警告码的完整定义、分类和可重试性以仓库的 `docs/protocol/errors.md` 和 `@video-lab/protocol` 的 `ERROR_META` 为准;不要自行拼错误文案或新增 code。
|
|
24
|
+
|
|
25
|
+
### QoE 的具体行为
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
`TelemetrySessionSummary.qoe` 覆盖:
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
- `firstFrameMs`:首帧可见耗时;
|
|
30
|
+
- `playbackStalls`:只累计 `kind='playback'` 的已结束卡顿,不把首播或 seek 等待混入;
|
|
31
|
+
- `errors`:错误次数和有限错误码集合;
|
|
32
|
+
- `qualityChanges`:自动与手动清晰度切换次数;
|
|
33
|
+
- `sourceSwitches`:授权换源的尝试、成功/失败/取消、热切/重建和耗时;
|
|
34
|
+
- `recovery`:恢复轮次、策略、终态和不完整/溢出事实;
|
|
35
|
+
- `health.kernel`、`health.audio`、`health.buffer`、`health.frameFreezes`:内核、音频、缓冲和帧冻结健康信号。
|
|
36
|
+
|
|
37
|
+
某个平台没有量具时,值是 `{ status: 'unknown' }`,不是 `0`。例如帧冻结目前是 PC-only;把缺失填成零会把覆盖率问题误判为体验良好。
|
|
38
|
+
|
|
39
|
+
## 最小接入
|
|
40
|
+
|
|
41
|
+
将唯一完整 `PlayerEvent` 事件流交给同一个会话。新版 SDK 事件的顶层 `delivery` 含生产端的 session、delivery ID、时间和 sequence,可让接收端可靠排序、去重和识别迟到事件。
|
|
18
42
|
|
|
19
43
|
<!-- doc-snippet-preamble
|
|
20
44
|
import type { PlayerEvent } from '@video-lab/protocol'
|
|
21
45
|
declare const BUILD_SHA: string
|
|
22
|
-
declare const event: PlayerEvent
|
|
23
|
-
declare const deliveredEvent: import('@video-lab/protocol').DeliveredPlayerEvent
|
|
24
46
|
declare function queueForDelivery(input: unknown): void
|
|
25
47
|
declare function queueSessionSummary(input: unknown): void
|
|
26
48
|
-->
|
|
@@ -29,6 +51,7 @@ declare function queueSessionSummary(input: unknown): void
|
|
|
29
51
|
import { createTelemetrySession } from '@video-lab/telemetry'
|
|
30
52
|
|
|
31
53
|
const telemetry = createTelemetrySession({
|
|
54
|
+
// release 是宿主应用版本,不是 telemetry 包或播放器包版本。
|
|
32
55
|
application: { name: 'web-console', release: BUILD_SHA },
|
|
33
56
|
sink: {
|
|
34
57
|
onRecord: queueForDelivery,
|
|
@@ -36,56 +59,152 @@ const telemetry = createTelemetrySession({
|
|
|
36
59
|
},
|
|
37
60
|
})
|
|
38
61
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
// React / Vue / 静态 iframe 统一使用完整事件出口。
|
|
63
|
+
function onPlayerEvent(event: PlayerEvent): void {
|
|
64
|
+
telemetry.record(event)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 业务自定义 UI、快捷键或 API 调用必须在发生处显式记录意图。
|
|
43
68
|
telemetry.recordIntent({ type: 'retry', origin: 'user' })
|
|
44
|
-
|
|
69
|
+
|
|
70
|
+
// 页面退出时才收口本会话,不要在创建 telemetry 后立即 finish。
|
|
71
|
+
window.addEventListener('pagehide', () => {
|
|
72
|
+
telemetry.finish({ reason: 'page_unload' })
|
|
73
|
+
}, { once: true })
|
|
45
74
|
```
|
|
46
75
|
|
|
47
|
-
|
|
76
|
+
五种接入面的完整入口分别是 React 的 `onPlayerEvent`、Vue 的 `@player-event`,以及静态 iframe 的 `embed.onAny()`。旧 iframe 事件若无 `delivery`,`record()` 会如实标注去重不可验证。不要只监听 `onStalled` 等单项 UI 回调来拼会话。
|
|
77
|
+
组件级播放器还应在 React effect cleanup 或 Vue `onScopeDispose()` 中以 `reason: 'destroyed'`
|
|
78
|
+
收口;静态 iframe 同时退订监听器并销毁控制器。
|
|
79
|
+
|
|
80
|
+
## 宿主需要准备什么
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
会被移除;不会输出 URL query、签名、cookie、user ID 或业务 tags。
|
|
82
|
+
`application.name` 是稳定的宿主名称;`application.release` 是宿主构建/发布版本,未提供时本包不会猜测。这样上报系统可以按“哪个业务应用的哪个版本”分组。
|
|
51
83
|
|
|
52
|
-
|
|
84
|
+
播放器包版本、iframe artifact 版本、业务意图、用户/内容关联等不属于 `TelemetryApplication`。需要它们时,在宿主传输封装中添加,而不要伪造成 SDK 字段或反向塞进播放器配置:
|
|
85
|
+
|
|
86
|
+
<!-- doc-snippet-preamble
|
|
87
|
+
declare const packageRelease: string
|
|
88
|
+
declare const iframeArtifactVersion: string | undefined
|
|
89
|
+
-->
|
|
53
90
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
91
|
+
```ts
|
|
92
|
+
import type { TelemetryRecord, TelemetrySessionSummary } from '@video-lab/telemetry'
|
|
93
|
+
|
|
94
|
+
type HostTelemetryEnvelope = {
|
|
95
|
+
receivedAtMs: number
|
|
96
|
+
player: {
|
|
97
|
+
packageRelease: string
|
|
98
|
+
iframeArtifactRelease?: string
|
|
99
|
+
}
|
|
100
|
+
payload:
|
|
101
|
+
| { kind: 'record'; value: TelemetryRecord }
|
|
102
|
+
| { kind: 'summary'; value: TelemetrySessionSummary }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function wrapRecord(record: TelemetryRecord): HostTelemetryEnvelope {
|
|
106
|
+
return {
|
|
107
|
+
receivedAtMs: Date.now(),
|
|
108
|
+
player: { packageRelease, iframeArtifactRelease: iframeArtifactVersion },
|
|
109
|
+
payload: { kind: 'record', value: record },
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
57
113
|
|
|
58
|
-
|
|
59
|
-
`source_changed` 收口旧会话,并开始新会话。`TelemetrySessionSummary.qoe` 中的每一项都是
|
|
60
|
-
`{ status: 'observed', value }` 或 `{ status: 'unknown' }`;没有收到平台专属信号(例如 PC-only
|
|
61
|
-
`framefreeze`)时绝不会伪造 `0`。
|
|
114
|
+
接收端至少应保存下列字段:
|
|
62
115
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
116
|
+
| 字段 | 来源 | 用途 |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `application.name` / `application.release` | 创建 session 时由宿主提供 | 业务应用和宿主版本分桶。 |
|
|
119
|
+
| `eventId`、`occurredAtMs`、`sequence` | telemetry record | 幂等、排序、排查迟到事件。 |
|
|
120
|
+
| `producerDelivery` | `record(event)` 读取 `event.delivery` | iframe 跨重连时的稳定去重证据;旧事件没有时不能臆造。 |
|
|
121
|
+
| `context.sessionId`、kernel、媒体类型、脱敏 origin/path | SDK 实际上下文 | 会话、内核、内容路径归因。 |
|
|
122
|
+
| `event` / `intent` | player record | 原始播放器事实或受限业务动作。 |
|
|
123
|
+
| `qoe`、`support`、`startup`、`completeness` | session summary | QoE 聚合、能力 coverage 与数据可信度判断。 |
|
|
124
|
+
| 外层 `player.packageRelease` / `iframeArtifactRelease` | 宿主传输封装 | 播放器、iframe 版本的发布回归分析。 |
|
|
68
125
|
|
|
69
|
-
|
|
126
|
+
不要把 URL query/hash、签名、token、cookie、`userId`、任意业务 tags、原始 `error.cause` 或内核自由文本放进 record。包已经移除一部分敏感信息;宿主出口仍要按自己的隐私规范做二次控制。
|
|
70
127
|
|
|
71
|
-
|
|
72
|
-
`user` / `api` origin。任意额外字段不会进入 record。sink 的异常被隔离,不会中断播放器;摘要的
|
|
73
|
-
`completeness` 会报告 context、delivery-id 或 sink 证据缺口。
|
|
128
|
+
## 输出结构与行为边界
|
|
74
129
|
|
|
75
|
-
`onRecord`
|
|
130
|
+
`onRecord` 接收两种 `TelemetryRecord`:
|
|
76
131
|
|
|
77
|
-
|
|
78
|
-
|
|
132
|
+
| `kind` | 结构 | 何时产生 |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `player_event` | 公共基字段 + 安全 `PlayerEvent`;有生产端证据时包含 `producerDelivery` | 每一个通过校验的原始播放器事件。 |
|
|
135
|
+
| `intent` | 公共基字段 + `{ type, origin }` | 宿主调用 `recordIntent()`。 |
|
|
79
136
|
|
|
80
|
-
|
|
137
|
+
公共基字段为 `eventId`、`schemaVersion: '2.0'`、`occurredAtMs`、递增 `sequence`、`application`、可选 `context`、`delivery: 'stable' | 'generated'` 和 `contextStatus: 'observed' | 'unattributed'`。
|
|
81
138
|
|
|
139
|
+
`onSummary` 接收一份 `TelemetrySessionSummary`:`support` 是实际选源事实,`startup` 只有首帧才为 `started`;不可重试首帧前错误或构造前不支持才为 `failed`,其余未知情况保持 `unknown`。`completeness` 会表明 `deduplication_unknown`、`context_unattributed`、`sink_failure`、未闭合卡顿/换源、恢复不完整、本地溢出或非法事件等缺口。接收端不能把 `partial` summary 当成完整 QoE 样本。
|
|
82
140
|
|
|
83
|
-
|
|
141
|
+
行为观测分两部分:SDK 内置控件的 `useraction` 已在完整 `PlayerEvent` 中;宿主自定义按钮、快捷键和 API 调用应只写 `recordIntent({ type, origin })`。允许的 type 是 `play`、`pause`、`seek`、`retry`、`source_change`、`exit`,origin 是 `user` 或 `api`;任意 tags、用户 ID 和自由 payload 都不会进入 telemetry。
|
|
84
142
|
|
|
85
|
-
`
|
|
143
|
+
`recovery.recovered` 不是收到了单个 `playing` / `canplay` 就成立:它需要 `playing_position_advance` 验证。403/鉴权错误也不会自动变成“已恢复”;宿主刷新签名、换源或业务重试后,仍应以 recovery 生命周期的真实终态判断。
|
|
86
144
|
|
|
87
|
-
|
|
145
|
+
## 高频与网络安全:宿主必须做的事
|
|
88
146
|
|
|
89
|
-
|
|
147
|
+
`onRecord` 是**本地观察回调**,不是“一次回调发一次 HTTP”的授权。每个有效 `PlayerEvent` 都会调用它;`timeupdate` 常见约 4 次/秒,即单播放器约 240 次/分钟。包内的 128/1024 等上限是会话聚合和 delivery cache 的内存边界,**不是**网络批量、节流、采样或重试策略。
|
|
90
148
|
|
|
91
|
-
|
|
149
|
+
推荐按优先级分流:
|
|
150
|
+
|
|
151
|
+
| 优先级 | 数据 | 建议宿主处理 |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| P0 | 不可重试 error、`recovery.failed` | 进入错误平台;可立即发送,但要去重、限并发和保留失败计数。 |
|
|
154
|
+
| P1 | `finish()` summary、已结束卡顿、健康退化、换源终态 | 入有界队列,按批量或短周期发送到 QoE/数仓管道。 |
|
|
155
|
+
| P2 | `timeupdate`、progress、一般播放事件、逐条 intent | 默认不逐条上网;只作本地聚合、抽样或按会话摘要消费。 |
|
|
156
|
+
|
|
157
|
+
下面是 sink 的接口样板。`HostTelemetryQueue` 必须由业务基础设施实现;它的 `enqueue()` 应是同步、快速且有界的,`flush()` 才进行网络工作。`onSummary` 主动 flush,确保会话结束尽快出队:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import type {
|
|
161
|
+
TelemetryRecord,
|
|
162
|
+
TelemetrySessionSummary,
|
|
163
|
+
TelemetrySink,
|
|
164
|
+
} from '@video-lab/telemetry'
|
|
165
|
+
|
|
166
|
+
type QueuedTelemetry =
|
|
167
|
+
| { kind: 'record'; value: TelemetryRecord }
|
|
168
|
+
| { kind: 'summary'; value: TelemetrySessionSummary }
|
|
169
|
+
|
|
170
|
+
interface HostTelemetryQueue {
|
|
171
|
+
enqueue(item: QueuedTelemetry): void
|
|
172
|
+
flush(): Promise<void>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createBatchedTelemetrySink(queue: HostTelemetryQueue): TelemetrySink {
|
|
176
|
+
return {
|
|
177
|
+
onRecord(record) {
|
|
178
|
+
queue.enqueue({ kind: 'record', value: record })
|
|
179
|
+
},
|
|
180
|
+
async onSummary(summary) {
|
|
181
|
+
queue.enqueue({ kind: 'summary', value: summary })
|
|
182
|
+
await queue.flush()
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
实现这个队列时,至少明确并监控以下规则:
|
|
189
|
+
|
|
190
|
+
- 最大队列、最大 batch、最大 in-flight 数;满载时按优先级丢弃 P2,不能静默挤掉 P0/P1;
|
|
191
|
+
- 定时(例如 5–10 秒)或达到 batch 大小时 flush,页面卸载时尽力使用 `sendBeacon` / keepalive;
|
|
192
|
+
- 有上限的指数退避和重试次数,失败后停止无限重放;
|
|
193
|
+
- 上报 queue overflow、丢弃量、上传失败、重试次数和延迟;它们不是播放器 QoE;
|
|
194
|
+
- 服务端按 `eventId` 或稳定 `producerDelivery.deliveryId` 做幂等,并限制单请求 body/事件数。
|
|
195
|
+
|
|
196
|
+
sink 的同步异常和异步拒绝会被 telemetry 隔离,不会中断播放器;相应会话的 `completeness.sinkFailures` 会记录事实。但后台队列自己的失败发生在 sink 返回之后,必须由宿主队列自行监控,不能期待 telemetry 替你观测网络送达。
|
|
197
|
+
|
|
198
|
+
## 会话生命周期与限制
|
|
199
|
+
|
|
200
|
+
使用 `record(event)`;它从 `event.delivery` 读取 producer session 和 delivery ID 去重并保留生产端证据。若宿主从外部获得的事件没有该字段,仍可用 `record(event, { deliveryId })` 注入稳定 ID;没有该 ID 时,相同 payload 可能是两次真实媒体事实,本包不会猜测并丢弃它。
|
|
201
|
+
|
|
202
|
+
`finish()` 幂等;同一会话重复调用返回同一份 summary。新 `contextchange.sessionId` 会以 `source_changed` 收口旧会话并开始新会话。每会话最多保留 128 个恢复轮次、每轮 64 个动作标识、1024 个 delivery cache、128 条 intent 和 128 个 ready 轨道项;溢出会如实写入 `localOverflow` / `local_overflow`,不会把信息缺失伪造为成功。
|
|
203
|
+
|
|
204
|
+
## 与 Sentry 的关系
|
|
205
|
+
|
|
206
|
+
本包不是 Sentry adapter,也不应把 `@sentry/*` 依赖带进它。业务应用可把不可重试 error 映射为 exception,`recovery.failed` 映射为去重后的 message,把已结束卡顿/健康退化作为低频 breadcrumb,并将 summary 发往 QoE 管道。不要把每个 `timeupdate` 写成 breadcrumb;它会很快冲掉真正的错误前线索。
|
|
207
|
+
|
|
208
|
+
参考实现位于 `examples/team-video-vue/reporting/sentry-telemetry-sink.ts`;跨框架绑定方式见仓库的 `docs/guides/USER-GUIDE.md` 与 `docs/guides/PLAYER-OBSERVABILITY-QOE.md`。
|
|
209
|
+
|
|
210
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -108,6 +108,18 @@ function createAggregate() {
|
|
|
108
108
|
errorCodes: [],
|
|
109
109
|
qualityChangeCount: 0,
|
|
110
110
|
autoQualityChangeCount: 0,
|
|
111
|
+
sourceSwitchAttempts: 0,
|
|
112
|
+
sourceSwitchSucceeded: 0,
|
|
113
|
+
sourceSwitchFailed: 0,
|
|
114
|
+
sourceSwitchCancelled: 0,
|
|
115
|
+
sourceSwitchHotLoad: 0,
|
|
116
|
+
sourceSwitchRebuild: 0,
|
|
117
|
+
sourceSwitchElapsedMs: {
|
|
118
|
+
succeeded: 0,
|
|
119
|
+
failed: 0,
|
|
120
|
+
cancelled: 0
|
|
121
|
+
},
|
|
122
|
+
openSourceSwitches: /* @__PURE__ */ new Set(),
|
|
111
123
|
frameFreezeCount: 0,
|
|
112
124
|
frameFreezeDurationMs: 0,
|
|
113
125
|
intents: []
|
|
@@ -236,6 +248,7 @@ function createTelemetrySession(options) {
|
|
|
236
248
|
*/
|
|
237
249
|
let seenDeliveries = /* @__PURE__ */ new Map();
|
|
238
250
|
let pendingSourceRoute;
|
|
251
|
+
let pendingSourceSwitches = /* @__PURE__ */ new Set();
|
|
239
252
|
let aggregate = createAggregate();
|
|
240
253
|
let sequence = 0;
|
|
241
254
|
let completedSummary;
|
|
@@ -266,7 +279,7 @@ function createTelemetrySession(options) {
|
|
|
266
279
|
function emitRecord(record) {
|
|
267
280
|
deliver(aggregate, () => options.sink?.onRecord?.(record));
|
|
268
281
|
}
|
|
269
|
-
function recordEventFact(event) {
|
|
282
|
+
function recordEventFact(event, sourceSwitchScope) {
|
|
270
283
|
if (event.event === "firstframe") aggregate.firstFrameMs ??= event.payload.fvt;
|
|
271
284
|
if (event.event === "stalled" && event.payload.kind === "playback") {
|
|
272
285
|
aggregate.incompleteStall ||= event.payload.phase === "start" && aggregate.unclosedStall || event.payload.phase === "end" && event.payload.durationMs === void 0;
|
|
@@ -285,6 +298,32 @@ function createTelemetrySession(options) {
|
|
|
285
298
|
aggregate.qualityChangeCount += 1;
|
|
286
299
|
if (event.payload.auto) aggregate.autoQualityChangeCount += 1;
|
|
287
300
|
}
|
|
301
|
+
if (event.event === "sourceswitch") {
|
|
302
|
+
const key = `${sourceSwitchScope}:${event.payload.switchId}`;
|
|
303
|
+
if (event.payload.phase === "started") {
|
|
304
|
+
if (!aggregate.openSourceSwitches.has(key)) {
|
|
305
|
+
aggregate.openSourceSwitches.add(key);
|
|
306
|
+
aggregate.sourceSwitchAttempts += 1;
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!aggregate.openSourceSwitches.delete(key)) return;
|
|
311
|
+
if (event.payload.phase === "succeeded") {
|
|
312
|
+
aggregate.sourceSwitchSucceeded += 1;
|
|
313
|
+
aggregate.sourceSwitchElapsedMs.succeeded += event.payload.elapsedMs;
|
|
314
|
+
if (event.payload.strategy === "hot_load") aggregate.sourceSwitchHotLoad += 1;
|
|
315
|
+
else aggregate.sourceSwitchRebuild += 1;
|
|
316
|
+
}
|
|
317
|
+
if (event.payload.phase === "failed") {
|
|
318
|
+
aggregate.sourceSwitchFailed += 1;
|
|
319
|
+
aggregate.sourceSwitchElapsedMs.failed += event.payload.elapsedMs;
|
|
320
|
+
}
|
|
321
|
+
if (event.payload.phase === "cancelled") {
|
|
322
|
+
aggregate.sourceSwitchCancelled += 1;
|
|
323
|
+
aggregate.sourceSwitchElapsedMs.cancelled += event.payload.elapsedMs;
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
288
327
|
if (event.event === "recovery") aggregate.recovery.record(event.payload);
|
|
289
328
|
if (event.event === "kernelhealth") aggregate.kernelHealth = {
|
|
290
329
|
degraded: event.payload.degraded,
|
|
@@ -311,6 +350,7 @@ function createTelemetrySession(options) {
|
|
|
311
350
|
if (recovery?.overflowEvents || aggregate.localOverflow) missing.push("local_overflow");
|
|
312
351
|
if (aggregate.invalidEvents) missing.push("invalid_event");
|
|
313
352
|
if (aggregate.unclosedStall || aggregate.incompleteStall) missing.push("unclosed_stall");
|
|
353
|
+
if (aggregate.openSourceSwitches.size > 0) missing.push("unclosed_source_switch");
|
|
314
354
|
const contextUnattributed = aggregate.unattributedRecord || !aggregate.currentContext;
|
|
315
355
|
if (aggregate.generatedDelivery) missing.push("deduplication_unknown");
|
|
316
356
|
if (contextUnattributed) missing.push("context_unattributed");
|
|
@@ -341,6 +381,15 @@ function createTelemetrySession(options) {
|
|
|
341
381
|
autoCount: aggregate.autoQualityChangeCount,
|
|
342
382
|
manualCount: aggregate.qualityChangeCount - aggregate.autoQualityChangeCount
|
|
343
383
|
}),
|
|
384
|
+
sourceSwitches: aggregate.sourceSwitchAttempts === 0 ? unknown() : observed({
|
|
385
|
+
attempts: aggregate.sourceSwitchAttempts,
|
|
386
|
+
succeeded: aggregate.sourceSwitchSucceeded,
|
|
387
|
+
failed: aggregate.sourceSwitchFailed,
|
|
388
|
+
cancelled: aggregate.sourceSwitchCancelled,
|
|
389
|
+
hotLoad: aggregate.sourceSwitchHotLoad,
|
|
390
|
+
rebuild: aggregate.sourceSwitchRebuild,
|
|
391
|
+
elapsedMs: aggregate.sourceSwitchElapsedMs
|
|
392
|
+
}),
|
|
344
393
|
recovery: recovery ? observed(recovery) : unknown(),
|
|
345
394
|
health: {
|
|
346
395
|
kernel: aggregate.kernelHealth ? observed(aggregate.kernelHealth) : unknown(),
|
|
@@ -382,16 +431,26 @@ function createTelemetrySession(options) {
|
|
|
382
431
|
}
|
|
383
432
|
const parsed = validation.data;
|
|
384
433
|
if (completedSummary) prepareForInput();
|
|
385
|
-
const
|
|
386
|
-
const
|
|
434
|
+
const producerDelivery = parsed.delivery ?? metadata?.producerDelivery;
|
|
435
|
+
const deliveryId = producerDelivery?.deliveryId ?? metadata?.deliveryId;
|
|
387
436
|
const identitySession = "sessionId" in parsed.payload ? parsed.payload.sessionId : aggregate.currentContext?.sessionId ?? aggregate.sourceRoute?.sessionId ?? "";
|
|
388
437
|
const deliveryKey = JSON.stringify(producerDelivery ? [producerDelivery.producerSessionId, producerDelivery.deliveryId] : [identitySession, deliveryId]);
|
|
389
438
|
if (deliveryId) {
|
|
390
439
|
const duplicate = seenDeliveries.get(deliveryKey);
|
|
391
440
|
if (duplicate) return duplicate;
|
|
392
441
|
}
|
|
393
|
-
if (parsed.event === "contextchange" && aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId && !completedSummary)
|
|
442
|
+
if (parsed.event === "contextchange" && aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId && !completedSummary) {
|
|
443
|
+
pendingSourceSwitches = new Set(aggregate.openSourceSwitches);
|
|
444
|
+
aggregate.sourceSwitchAttempts -= pendingSourceSwitches.size;
|
|
445
|
+
aggregate.openSourceSwitches.clear();
|
|
446
|
+
finish("source_changed");
|
|
447
|
+
}
|
|
394
448
|
prepareForInput();
|
|
449
|
+
if (pendingSourceSwitches.size > 0) {
|
|
450
|
+
aggregate.openSourceSwitches = pendingSourceSwitches;
|
|
451
|
+
aggregate.sourceSwitchAttempts += pendingSourceSwitches.size;
|
|
452
|
+
pendingSourceSwitches = /* @__PURE__ */ new Set();
|
|
453
|
+
}
|
|
395
454
|
const isPendingSourceRoute = parsed.event === "sourceroute" && !!aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId;
|
|
396
455
|
const currentSessionId = aggregate.currentContext?.sessionId ?? aggregate.sourceRoute?.sessionId;
|
|
397
456
|
const isForeignRecovery = parsed.event === "recovery" && currentSessionId !== void 0 && currentSessionId !== parsed.payload.sessionId;
|
|
@@ -423,7 +482,7 @@ function createTelemetrySession(options) {
|
|
|
423
482
|
aggregate.eventCount += 1;
|
|
424
483
|
aggregate.generatedDelivery ||= !deliveryId;
|
|
425
484
|
aggregate.unattributedRecord ||= record.contextStatus === "unattributed" && parsed.event !== "sourceroute";
|
|
426
|
-
recordEventFact(record.event);
|
|
485
|
+
recordEventFact(record.event, producerDelivery?.producerSessionId ?? "raw");
|
|
427
486
|
}
|
|
428
487
|
if (deliveryId) if (seenDeliveries.size < 1024) seenDeliveries.set(deliveryKey, record);
|
|
429
488
|
else {
|
|
@@ -433,18 +492,6 @@ function createTelemetrySession(options) {
|
|
|
433
492
|
emitRecord(record);
|
|
434
493
|
return record;
|
|
435
494
|
},
|
|
436
|
-
recordDelivered(event) {
|
|
437
|
-
return this.record(event.event, {
|
|
438
|
-
deliveryId: event.deliveryId,
|
|
439
|
-
occurredAtMs: event.occurredAtMs,
|
|
440
|
-
producerDelivery: {
|
|
441
|
-
producerSessionId: event.producerSessionId,
|
|
442
|
-
deliveryId: event.deliveryId,
|
|
443
|
-
occurredAtMs: event.occurredAtMs,
|
|
444
|
-
sequence: event.sequence
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
},
|
|
448
495
|
recordIntent(intent) {
|
|
449
496
|
prepareForInput();
|
|
450
497
|
const projectedIntent = projectIntent(intent);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrorCode, PlaybackContext, PlayerEvent, RecoveryPayload, SourceRoutePayload } from "@video-lab/protocol";
|
|
2
2
|
//#region src/recovery-summary.d.ts
|
|
3
3
|
type Terminal = Extract<RecoveryPayload, {
|
|
4
4
|
phase: 'recovered' | 'failed' | 'cancelled';
|
|
@@ -40,7 +40,7 @@ interface TelemetryRecordOptions {
|
|
|
40
40
|
deliveryId?: string;
|
|
41
41
|
/** 事件在播放器侧发生的时间;缺省时由会话时钟采样。 */
|
|
42
42
|
occurredAtMs?: number;
|
|
43
|
-
/**
|
|
43
|
+
/** 外部注入的生产端投递证据;SDK 事件优先读取 `event.delivery`。 */
|
|
44
44
|
producerDelivery?: TelemetryProducerDelivery;
|
|
45
45
|
}
|
|
46
46
|
/** 可随 telemetry record 外发的生产端排序、去重与迟到识别证据。 */
|
|
@@ -70,7 +70,7 @@ interface TelemetryRecordBase {
|
|
|
70
70
|
interface TelemetryPlayerRecord extends TelemetryRecordBase {
|
|
71
71
|
kind: 'player_event';
|
|
72
72
|
event: PlayerEvent;
|
|
73
|
-
/**
|
|
73
|
+
/** 有生产端投递证据的事件提供;无证据事件省略。 */
|
|
74
74
|
producerDelivery?: TelemetryProducerDelivery;
|
|
75
75
|
}
|
|
76
76
|
interface TelemetryIntentRecord extends TelemetryRecordBase {
|
|
@@ -128,6 +128,19 @@ interface TelemetrySessionSummary {
|
|
|
128
128
|
autoCount: number;
|
|
129
129
|
manualCount: number;
|
|
130
130
|
}>;
|
|
131
|
+
sourceSwitches: TelemetryValue<{
|
|
132
|
+
attempts: number;
|
|
133
|
+
succeeded: number;
|
|
134
|
+
failed: number;
|
|
135
|
+
cancelled: number;
|
|
136
|
+
hotLoad: number;
|
|
137
|
+
rebuild: number;
|
|
138
|
+
elapsedMs: {
|
|
139
|
+
succeeded: number;
|
|
140
|
+
failed: number;
|
|
141
|
+
cancelled: number;
|
|
142
|
+
};
|
|
143
|
+
}>;
|
|
131
144
|
recovery: TelemetryValue<TelemetryRecoverySummary>;
|
|
132
145
|
health: {
|
|
133
146
|
kernel: TelemetryValue<{
|
|
@@ -151,7 +164,7 @@ interface TelemetrySessionSummary {
|
|
|
151
164
|
intents: TelemetryValue<readonly TelemetryIntent[]>;
|
|
152
165
|
completeness: {
|
|
153
166
|
status: 'complete' | 'partial';
|
|
154
|
-
missing: Array<'deduplication_unknown' | 'context_unattributed' | 'sink_failure' | 'unclosed_episode' | 'recovery_incomplete' | 'local_overflow' | 'invalid_event' | 'unclosed_stall'>;
|
|
167
|
+
missing: Array<'deduplication_unknown' | 'context_unattributed' | 'sink_failure' | 'unclosed_episode' | 'recovery_incomplete' | 'local_overflow' | 'invalid_event' | 'unclosed_stall' | 'unclosed_source_switch'>;
|
|
155
168
|
deduplication: 'verified' | 'unknown';
|
|
156
169
|
context: 'observed' | 'unattributed';
|
|
157
170
|
sinkFailures: number;
|
|
@@ -172,8 +185,6 @@ interface CreateTelemetrySessionOptions {
|
|
|
172
185
|
interface TelemetrySession {
|
|
173
186
|
/** 把完整 PlayerEvent 归一成可重试、可排序的安全 record。 */
|
|
174
187
|
record(event: unknown, options?: TelemetryRecordOptions): TelemetryPlayerRecord | null;
|
|
175
|
-
/** 直接消费五面同构的 producer delivery 证据。 */
|
|
176
|
-
recordDelivered(event: DeliveredPlayerEvent): TelemetryPlayerRecord | null;
|
|
177
188
|
/** 记录有限、脱敏的宿主意图,不把任意业务数据带进遥测。 */
|
|
178
189
|
recordIntent(intent: TelemetryIntent): TelemetryIntentRecord;
|
|
179
190
|
/** 关闭当前播放会话;重复调用返回同一份 summary。 */
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrorCode, PlaybackContext, PlayerEvent, RecoveryPayload, SourceRoutePayload } from "@video-lab/protocol";
|
|
2
2
|
//#region src/recovery-summary.d.ts
|
|
3
3
|
type Terminal = Extract<RecoveryPayload, {
|
|
4
4
|
phase: 'recovered' | 'failed' | 'cancelled';
|
|
@@ -40,7 +40,7 @@ interface TelemetryRecordOptions {
|
|
|
40
40
|
deliveryId?: string;
|
|
41
41
|
/** 事件在播放器侧发生的时间;缺省时由会话时钟采样。 */
|
|
42
42
|
occurredAtMs?: number;
|
|
43
|
-
/**
|
|
43
|
+
/** 外部注入的生产端投递证据;SDK 事件优先读取 `event.delivery`。 */
|
|
44
44
|
producerDelivery?: TelemetryProducerDelivery;
|
|
45
45
|
}
|
|
46
46
|
/** 可随 telemetry record 外发的生产端排序、去重与迟到识别证据。 */
|
|
@@ -70,7 +70,7 @@ interface TelemetryRecordBase {
|
|
|
70
70
|
interface TelemetryPlayerRecord extends TelemetryRecordBase {
|
|
71
71
|
kind: 'player_event';
|
|
72
72
|
event: PlayerEvent;
|
|
73
|
-
/**
|
|
73
|
+
/** 有生产端投递证据的事件提供;无证据事件省略。 */
|
|
74
74
|
producerDelivery?: TelemetryProducerDelivery;
|
|
75
75
|
}
|
|
76
76
|
interface TelemetryIntentRecord extends TelemetryRecordBase {
|
|
@@ -128,6 +128,19 @@ interface TelemetrySessionSummary {
|
|
|
128
128
|
autoCount: number;
|
|
129
129
|
manualCount: number;
|
|
130
130
|
}>;
|
|
131
|
+
sourceSwitches: TelemetryValue<{
|
|
132
|
+
attempts: number;
|
|
133
|
+
succeeded: number;
|
|
134
|
+
failed: number;
|
|
135
|
+
cancelled: number;
|
|
136
|
+
hotLoad: number;
|
|
137
|
+
rebuild: number;
|
|
138
|
+
elapsedMs: {
|
|
139
|
+
succeeded: number;
|
|
140
|
+
failed: number;
|
|
141
|
+
cancelled: number;
|
|
142
|
+
};
|
|
143
|
+
}>;
|
|
131
144
|
recovery: TelemetryValue<TelemetryRecoverySummary>;
|
|
132
145
|
health: {
|
|
133
146
|
kernel: TelemetryValue<{
|
|
@@ -151,7 +164,7 @@ interface TelemetrySessionSummary {
|
|
|
151
164
|
intents: TelemetryValue<readonly TelemetryIntent[]>;
|
|
152
165
|
completeness: {
|
|
153
166
|
status: 'complete' | 'partial';
|
|
154
|
-
missing: Array<'deduplication_unknown' | 'context_unattributed' | 'sink_failure' | 'unclosed_episode' | 'recovery_incomplete' | 'local_overflow' | 'invalid_event' | 'unclosed_stall'>;
|
|
167
|
+
missing: Array<'deduplication_unknown' | 'context_unattributed' | 'sink_failure' | 'unclosed_episode' | 'recovery_incomplete' | 'local_overflow' | 'invalid_event' | 'unclosed_stall' | 'unclosed_source_switch'>;
|
|
155
168
|
deduplication: 'verified' | 'unknown';
|
|
156
169
|
context: 'observed' | 'unattributed';
|
|
157
170
|
sinkFailures: number;
|
|
@@ -172,8 +185,6 @@ interface CreateTelemetrySessionOptions {
|
|
|
172
185
|
interface TelemetrySession {
|
|
173
186
|
/** 把完整 PlayerEvent 归一成可重试、可排序的安全 record。 */
|
|
174
187
|
record(event: unknown, options?: TelemetryRecordOptions): TelemetryPlayerRecord | null;
|
|
175
|
-
/** 直接消费五面同构的 producer delivery 证据。 */
|
|
176
|
-
recordDelivered(event: DeliveredPlayerEvent): TelemetryPlayerRecord | null;
|
|
177
188
|
/** 记录有限、脱敏的宿主意图,不把任意业务数据带进遥测。 */
|
|
178
189
|
recordIntent(intent: TelemetryIntent): TelemetryIntentRecord;
|
|
179
190
|
/** 关闭当前播放会话;重复调用返回同一份 summary。 */
|
package/dist/index.mjs
CHANGED
|
@@ -107,6 +107,18 @@ function createAggregate() {
|
|
|
107
107
|
errorCodes: [],
|
|
108
108
|
qualityChangeCount: 0,
|
|
109
109
|
autoQualityChangeCount: 0,
|
|
110
|
+
sourceSwitchAttempts: 0,
|
|
111
|
+
sourceSwitchSucceeded: 0,
|
|
112
|
+
sourceSwitchFailed: 0,
|
|
113
|
+
sourceSwitchCancelled: 0,
|
|
114
|
+
sourceSwitchHotLoad: 0,
|
|
115
|
+
sourceSwitchRebuild: 0,
|
|
116
|
+
sourceSwitchElapsedMs: {
|
|
117
|
+
succeeded: 0,
|
|
118
|
+
failed: 0,
|
|
119
|
+
cancelled: 0
|
|
120
|
+
},
|
|
121
|
+
openSourceSwitches: /* @__PURE__ */ new Set(),
|
|
110
122
|
frameFreezeCount: 0,
|
|
111
123
|
frameFreezeDurationMs: 0,
|
|
112
124
|
intents: []
|
|
@@ -235,6 +247,7 @@ function createTelemetrySession(options) {
|
|
|
235
247
|
*/
|
|
236
248
|
let seenDeliveries = /* @__PURE__ */ new Map();
|
|
237
249
|
let pendingSourceRoute;
|
|
250
|
+
let pendingSourceSwitches = /* @__PURE__ */ new Set();
|
|
238
251
|
let aggregate = createAggregate();
|
|
239
252
|
let sequence = 0;
|
|
240
253
|
let completedSummary;
|
|
@@ -265,7 +278,7 @@ function createTelemetrySession(options) {
|
|
|
265
278
|
function emitRecord(record) {
|
|
266
279
|
deliver(aggregate, () => options.sink?.onRecord?.(record));
|
|
267
280
|
}
|
|
268
|
-
function recordEventFact(event) {
|
|
281
|
+
function recordEventFact(event, sourceSwitchScope) {
|
|
269
282
|
if (event.event === "firstframe") aggregate.firstFrameMs ??= event.payload.fvt;
|
|
270
283
|
if (event.event === "stalled" && event.payload.kind === "playback") {
|
|
271
284
|
aggregate.incompleteStall ||= event.payload.phase === "start" && aggregate.unclosedStall || event.payload.phase === "end" && event.payload.durationMs === void 0;
|
|
@@ -284,6 +297,32 @@ function createTelemetrySession(options) {
|
|
|
284
297
|
aggregate.qualityChangeCount += 1;
|
|
285
298
|
if (event.payload.auto) aggregate.autoQualityChangeCount += 1;
|
|
286
299
|
}
|
|
300
|
+
if (event.event === "sourceswitch") {
|
|
301
|
+
const key = `${sourceSwitchScope}:${event.payload.switchId}`;
|
|
302
|
+
if (event.payload.phase === "started") {
|
|
303
|
+
if (!aggregate.openSourceSwitches.has(key)) {
|
|
304
|
+
aggregate.openSourceSwitches.add(key);
|
|
305
|
+
aggregate.sourceSwitchAttempts += 1;
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!aggregate.openSourceSwitches.delete(key)) return;
|
|
310
|
+
if (event.payload.phase === "succeeded") {
|
|
311
|
+
aggregate.sourceSwitchSucceeded += 1;
|
|
312
|
+
aggregate.sourceSwitchElapsedMs.succeeded += event.payload.elapsedMs;
|
|
313
|
+
if (event.payload.strategy === "hot_load") aggregate.sourceSwitchHotLoad += 1;
|
|
314
|
+
else aggregate.sourceSwitchRebuild += 1;
|
|
315
|
+
}
|
|
316
|
+
if (event.payload.phase === "failed") {
|
|
317
|
+
aggregate.sourceSwitchFailed += 1;
|
|
318
|
+
aggregate.sourceSwitchElapsedMs.failed += event.payload.elapsedMs;
|
|
319
|
+
}
|
|
320
|
+
if (event.payload.phase === "cancelled") {
|
|
321
|
+
aggregate.sourceSwitchCancelled += 1;
|
|
322
|
+
aggregate.sourceSwitchElapsedMs.cancelled += event.payload.elapsedMs;
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
287
326
|
if (event.event === "recovery") aggregate.recovery.record(event.payload);
|
|
288
327
|
if (event.event === "kernelhealth") aggregate.kernelHealth = {
|
|
289
328
|
degraded: event.payload.degraded,
|
|
@@ -310,6 +349,7 @@ function createTelemetrySession(options) {
|
|
|
310
349
|
if (recovery?.overflowEvents || aggregate.localOverflow) missing.push("local_overflow");
|
|
311
350
|
if (aggregate.invalidEvents) missing.push("invalid_event");
|
|
312
351
|
if (aggregate.unclosedStall || aggregate.incompleteStall) missing.push("unclosed_stall");
|
|
352
|
+
if (aggregate.openSourceSwitches.size > 0) missing.push("unclosed_source_switch");
|
|
313
353
|
const contextUnattributed = aggregate.unattributedRecord || !aggregate.currentContext;
|
|
314
354
|
if (aggregate.generatedDelivery) missing.push("deduplication_unknown");
|
|
315
355
|
if (contextUnattributed) missing.push("context_unattributed");
|
|
@@ -340,6 +380,15 @@ function createTelemetrySession(options) {
|
|
|
340
380
|
autoCount: aggregate.autoQualityChangeCount,
|
|
341
381
|
manualCount: aggregate.qualityChangeCount - aggregate.autoQualityChangeCount
|
|
342
382
|
}),
|
|
383
|
+
sourceSwitches: aggregate.sourceSwitchAttempts === 0 ? unknown() : observed({
|
|
384
|
+
attempts: aggregate.sourceSwitchAttempts,
|
|
385
|
+
succeeded: aggregate.sourceSwitchSucceeded,
|
|
386
|
+
failed: aggregate.sourceSwitchFailed,
|
|
387
|
+
cancelled: aggregate.sourceSwitchCancelled,
|
|
388
|
+
hotLoad: aggregate.sourceSwitchHotLoad,
|
|
389
|
+
rebuild: aggregate.sourceSwitchRebuild,
|
|
390
|
+
elapsedMs: aggregate.sourceSwitchElapsedMs
|
|
391
|
+
}),
|
|
343
392
|
recovery: recovery ? observed(recovery) : unknown(),
|
|
344
393
|
health: {
|
|
345
394
|
kernel: aggregate.kernelHealth ? observed(aggregate.kernelHealth) : unknown(),
|
|
@@ -381,16 +430,26 @@ function createTelemetrySession(options) {
|
|
|
381
430
|
}
|
|
382
431
|
const parsed = validation.data;
|
|
383
432
|
if (completedSummary) prepareForInput();
|
|
384
|
-
const
|
|
385
|
-
const
|
|
433
|
+
const producerDelivery = parsed.delivery ?? metadata?.producerDelivery;
|
|
434
|
+
const deliveryId = producerDelivery?.deliveryId ?? metadata?.deliveryId;
|
|
386
435
|
const identitySession = "sessionId" in parsed.payload ? parsed.payload.sessionId : aggregate.currentContext?.sessionId ?? aggregate.sourceRoute?.sessionId ?? "";
|
|
387
436
|
const deliveryKey = JSON.stringify(producerDelivery ? [producerDelivery.producerSessionId, producerDelivery.deliveryId] : [identitySession, deliveryId]);
|
|
388
437
|
if (deliveryId) {
|
|
389
438
|
const duplicate = seenDeliveries.get(deliveryKey);
|
|
390
439
|
if (duplicate) return duplicate;
|
|
391
440
|
}
|
|
392
|
-
if (parsed.event === "contextchange" && aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId && !completedSummary)
|
|
441
|
+
if (parsed.event === "contextchange" && aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId && !completedSummary) {
|
|
442
|
+
pendingSourceSwitches = new Set(aggregate.openSourceSwitches);
|
|
443
|
+
aggregate.sourceSwitchAttempts -= pendingSourceSwitches.size;
|
|
444
|
+
aggregate.openSourceSwitches.clear();
|
|
445
|
+
finish("source_changed");
|
|
446
|
+
}
|
|
393
447
|
prepareForInput();
|
|
448
|
+
if (pendingSourceSwitches.size > 0) {
|
|
449
|
+
aggregate.openSourceSwitches = pendingSourceSwitches;
|
|
450
|
+
aggregate.sourceSwitchAttempts += pendingSourceSwitches.size;
|
|
451
|
+
pendingSourceSwitches = /* @__PURE__ */ new Set();
|
|
452
|
+
}
|
|
394
453
|
const isPendingSourceRoute = parsed.event === "sourceroute" && !!aggregate.currentContext && aggregate.currentContext.sessionId !== parsed.payload.sessionId;
|
|
395
454
|
const currentSessionId = aggregate.currentContext?.sessionId ?? aggregate.sourceRoute?.sessionId;
|
|
396
455
|
const isForeignRecovery = parsed.event === "recovery" && currentSessionId !== void 0 && currentSessionId !== parsed.payload.sessionId;
|
|
@@ -422,7 +481,7 @@ function createTelemetrySession(options) {
|
|
|
422
481
|
aggregate.eventCount += 1;
|
|
423
482
|
aggregate.generatedDelivery ||= !deliveryId;
|
|
424
483
|
aggregate.unattributedRecord ||= record.contextStatus === "unattributed" && parsed.event !== "sourceroute";
|
|
425
|
-
recordEventFact(record.event);
|
|
484
|
+
recordEventFact(record.event, producerDelivery?.producerSessionId ?? "raw");
|
|
426
485
|
}
|
|
427
486
|
if (deliveryId) if (seenDeliveries.size < 1024) seenDeliveries.set(deliveryKey, record);
|
|
428
487
|
else {
|
|
@@ -432,18 +491,6 @@ function createTelemetrySession(options) {
|
|
|
432
491
|
emitRecord(record);
|
|
433
492
|
return record;
|
|
434
493
|
},
|
|
435
|
-
recordDelivered(event) {
|
|
436
|
-
return this.record(event.event, {
|
|
437
|
-
deliveryId: event.deliveryId,
|
|
438
|
-
occurredAtMs: event.occurredAtMs,
|
|
439
|
-
producerDelivery: {
|
|
440
|
-
producerSessionId: event.producerSessionId,
|
|
441
|
-
deliveryId: event.deliveryId,
|
|
442
|
-
occurredAtMs: event.occurredAtMs,
|
|
443
|
-
sequence: event.sequence
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
},
|
|
447
494
|
recordIntent(intent) {
|
|
448
495
|
prepareForInput();
|
|
449
496
|
const projectedIntent = projectIntent(intent);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@video-lab/telemetry",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@video-lab/protocol": "
|
|
27
|
+
"@video-lab/protocol": "4.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsdown": "^0.22.9",
|