@spatius/avatarkit 1.3.1-beta.6 → 1.3.1

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.
Files changed (22) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/{AvatarDownloader-Dfi7wtgQ.js → AvatarDownloader-DyhenPKd.js} +98 -43
  3. package/dist/{AvatarSDK-CnZqa_X4.js → AvatarSDK-DgPvHU4L.js} +162 -2560
  4. package/dist/{OpusCodec-D9DcUV65.js → OpusCodec-CXU7ysrU.js} +43 -9
  5. package/dist/{OpusDecoderProxy-Ar_dFXRB.js → OpusDecoderProxy-Cq8Z_MZY.js} +19 -6
  6. package/dist/{OpusEncoderProxy-9YIZ7nIt.js → OpusEncoderProxy-CzY-VckE.js} +42 -7
  7. package/dist/{StreamingAudioPlayer-D4q7c1U0.js → StreamingAudioPlayer-CIJmBp3z.js} +2 -2
  8. package/dist/assets/{AvatarDownloader-D5GePpJ2.js → AvatarDownloader-G5FO1bVl.js} +96 -41
  9. package/dist/assets/{AvatarSDK-DR8dNylh.js → AvatarSDK-C_Anluwp.js} +149 -12
  10. package/dist/assets/{OpusDecoderWorker.worker-B7R93nev.js → OpusDecoderWorker.worker-C2T0afkj.js} +21 -5
  11. package/dist/assets/{OpusEncoderWorker.worker-BYWIV5xu.js → OpusEncoderWorker.worker-DLYO9vPK.js} +53 -11
  12. package/dist/assets/{logger-D8rDVim7.js → logger-YPooEAbA.js} +451 -132
  13. package/dist/core/AvatarController.d.ts +10 -0
  14. package/dist/{error-utils-CqKOrHrY.js → error-utils-BogEllAd.js} +1 -1
  15. package/dist/index.js +279 -46
  16. package/dist/internal-telemetry.d.ts +45 -20
  17. package/dist/internal-telemetry.js +27 -2
  18. package/dist/{logger-fAU8oahT.js → logger-BOpQ7u0w.js} +468 -132
  19. package/dist/otel-trace-Ct4T48nC.js +2565 -0
  20. package/dist/{pwa-cache-manager-CwXaM3xR.js → pwa-cache-manager-C7NbU52Q.js} +1 -1
  21. package/dist/types/index.d.ts +30 -4
  22. package/package.json +1 -1
@@ -1156,6 +1156,28 @@ let DrivingServiceMode = /* @__PURE__ */ function(DrivingServiceMode) {
1156
1156
  DrivingServiceMode["direct"] = "direct";
1157
1157
  /** Driven by host application */
1158
1158
  DrivingServiceMode["backend"] = "backend";
1159
+ /**
1160
+ * Use this **only** when driving the avatar through the companion RTC SDK,
1161
+ * `@spatius/avatarkit-rtc`. If you are not using that package, stay on
1162
+ * `direct` / `backend` — do not switch to this because it looks newer.
1163
+ *
1164
+ * **Telemetry dimension only — it gates no behaviour.** Every session reports
1165
+ * `dsm`; without a value of its own, RTC traffic would be indistinguishable
1166
+ * from plain `backend` traffic on the dashboards.
1167
+ *
1168
+ * It deliberately does not unlock any API. RTC drives the avatar frame by
1169
+ * frame through `AvatarView.renderFrame` / `renderFromProtobuf`, which put the
1170
+ * view into pure-rendering mode on their own — it never calls the host-driven
1171
+ * feeding path (`yieldAudioData` / `yieldFramesData`) that `backend` exists
1172
+ * for. So the `=== backend` checks guarding those methods are left untouched:
1173
+ * loosening them for `rtc` would widen the reachable API surface without
1174
+ * enabling anything RTC actually uses.
1175
+ *
1176
+ * Note the two concepts sit at different levels and are not interchangeable:
1177
+ * pure-rendering mode is a per-frame runtime state of the render loop, while
1178
+ * this is a session-wide value declared once at `initialize`.
1179
+ */
1180
+ DrivingServiceMode["rtc"] = "rtc";
1159
1181
  return DrivingServiceMode;
1160
1182
  }({});
1161
1183
  let LogLevel = /* @__PURE__ */ function(LogLevel) {
@@ -7111,7 +7133,7 @@ let customRandom = (alphabet, defaultSize, getRandom) => {
7111
7133
  };
7112
7134
  };
7113
7135
  let customAlphabet = (alphabet, size = 21) => customRandom(alphabet, size | 0, random);
7114
- let nanoid$1 = (size = 21) => {
7136
+ let nanoid$2 = (size = 21) => {
7115
7137
  let id = "";
7116
7138
  let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
7117
7139
  while (size--) id += urlAlphabet[bytes[size] & 63];
@@ -7136,11 +7158,11 @@ function getOrCreateClientId() {
7136
7158
  try {
7137
7159
  const stored = localStorage.getItem(CLIENT_ID_STORAGE_KEY);
7138
7160
  if (stored) return stored;
7139
- const clientId = nanoid$1(21);
7161
+ const clientId = nanoid$2(21);
7140
7162
  localStorage.setItem(CLIENT_ID_STORAGE_KEY, clientId);
7141
7163
  return clientId;
7142
7164
  } catch (error) {
7143
- return `temp_${nanoid$1(21)}_${Date.now()}`;
7165
+ return `temp_${nanoid$2(21)}_${Date.now()}`;
7144
7166
  }
7145
7167
  }
7146
7168
  //#endregion
@@ -7158,7 +7180,7 @@ function getOrCreateClientId() {
7158
7180
  * Custom character set: only uppercase, lowercase letters and numbers (URL safe, no hyphens)
7159
7181
  * Character set: A-Z, a-z, 0-9 (62 characters)
7160
7182
  */
7161
- const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 12);
7183
+ const nanoid$1 = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 12);
7162
7184
  /**
7163
7185
  * Generate unified conversation ID
7164
7186
  *
@@ -7173,7 +7195,7 @@ const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
7173
7195
  const generateTraceId = () => generateConversationId();
7174
7196
  function generateConversationId() {
7175
7197
  const now = /* @__PURE__ */ new Date();
7176
- return `${`${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`}_${nanoid()}`;
7198
+ return `${`${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`}_${nanoid$1()}`;
7177
7199
  }
7178
7200
  //#endregion
7179
7201
  //#region utils/id-manager.ts
@@ -7182,9 +7204,12 @@ function generateConversationId() {
7182
7204
  * Unified management of all types of IDs in SDK
7183
7205
  * @internal
7184
7206
  */
7207
+ /** 与 conversation_id 同字符集:URL 安全、无连字符。 */
7208
+ const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 12);
7185
7209
  var IdManager = class {
7186
7210
  ids = {
7187
7211
  clientId: "",
7212
+ sessionId: "",
7188
7213
  userId: null,
7189
7214
  appId: null,
7190
7215
  sessionToken: null,
@@ -7267,10 +7292,16 @@ var IdManager = class {
7267
7292
  getLogContext() {
7268
7293
  return {
7269
7294
  client_id: this.ids.clientId,
7295
+ session_id: this.ids.sessionId,
7270
7296
  user_id: this.ids.userId || "",
7271
7297
  app_id: this.ids.appId || ""
7272
7298
  };
7273
7299
  }
7300
+ /** 本次启动的 session id;首次调用时生成。 @internal */
7301
+ getSessionId() {
7302
+ if (!this.ids.sessionId) this.ids.sessionId = nanoid();
7303
+ return this.ids.sessionId;
7304
+ }
7274
7305
  /**
7275
7306
  * Clear all IDs (for testing or reset)
7276
7307
  * @internal
@@ -13329,119 +13360,6 @@ var OTLPLogExporter = class extends OTLPExporterBase {
13329
13360
  }
13330
13361
  };
13331
13362
  //#endregion
13332
- //#region utils/event-store.ts
13333
- /**
13334
- * Event Store - Persistent buffer for OTel telemetry events.
13335
- *
13336
- * Each emitted event is written here before being handed to the OTel
13337
- * BatchProcessor. When the wrapper exporter picks a batch up for HTTP
13338
- * upload it deletes those records — so anything still here on next
13339
- * startup is unsent and gets re-emitted (replay).
13340
- *
13341
- * Schema is intentionally minimal: no status / retry_count. A record's
13342
- * presence in the store means "not yet handed to inner.export".
13343
- * @internal
13344
- */
13345
- const DB_NAME = "avatarkit_events";
13346
- const STORE_NAME = "events";
13347
- const DB_VERSION = 1;
13348
- var EventStore = class {
13349
- db = null;
13350
- initPromise = null;
13351
- initialize() {
13352
- if (this.db) return Promise.resolve();
13353
- if (this.initPromise) return this.initPromise;
13354
- this.initPromise = new Promise((resolve, reject) => {
13355
- const req = indexedDB.open(DB_NAME, DB_VERSION);
13356
- req.onerror = () => reject(/* @__PURE__ */ new Error("Failed to open avatarkit_events"));
13357
- req.onsuccess = () => {
13358
- this.db = req.result;
13359
- resolve();
13360
- };
13361
- req.onupgradeneeded = (e) => {
13362
- const db = e.target.result;
13363
- if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, {
13364
- keyPath: "id",
13365
- autoIncrement: true
13366
- });
13367
- };
13368
- });
13369
- return this.initPromise;
13370
- }
13371
- /**
13372
- * Append a record, returning the auto-generated id.
13373
- * Caller must use this id as the `_index` attribute when emitting to OTel.
13374
- */
13375
- async add(record) {
13376
- if (!this.db) await this.initialize();
13377
- if (!this.db) throw new Error("event-store not available");
13378
- return new Promise((resolve, reject) => {
13379
- const req = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).add(record);
13380
- req.onsuccess = () => resolve(req.result);
13381
- req.onerror = () => reject(/* @__PURE__ */ new Error("event-store add failed"));
13382
- });
13383
- }
13384
- /**
13385
- * Atomically: for each id, check whether it still exists in the store;
13386
- * if so, delete it and include it in the returned set. Used by the
13387
- * wrapper exporter to claim ownership of a batch — only records the
13388
- * caller successfully "took" should be forwarded to inner.export.
13389
- *
13390
- * This is the multi-tab dedup mechanism: when Tab B starts and replays
13391
- * records that Tab A also still has in memory, only one tab will win
13392
- * the take and actually send.
13393
- *
13394
- * The get + delete pair runs inside a single readwrite transaction so
13395
- * concurrent tabs see consistent state.
13396
- */
13397
- async takeIfExists(ids) {
13398
- const taken = /* @__PURE__ */ new Set();
13399
- if (ids.length === 0) return taken;
13400
- if (!this.db) await this.initialize();
13401
- if (!this.db) return taken;
13402
- return new Promise((resolve) => {
13403
- const tx = this.db.transaction([STORE_NAME], "readwrite");
13404
- const store = tx.objectStore(STORE_NAME);
13405
- let pending = ids.length;
13406
- const finish = () => {
13407
- if (--pending === 0) resolve(taken);
13408
- };
13409
- for (const id of ids) {
13410
- const req = store.get(id);
13411
- req.onsuccess = () => {
13412
- if (req.result !== void 0) {
13413
- taken.add(id);
13414
- store.delete(id);
13415
- }
13416
- finish();
13417
- };
13418
- req.onerror = () => finish();
13419
- }
13420
- tx.onerror = () => {
13421
- logger.warn("[event-store] takeIfExists tx failed");
13422
- resolve(taken);
13423
- };
13424
- });
13425
- }
13426
- /**
13427
- * Return all stored records. Used on startup to replay anything left
13428
- * over from a previous (possibly crashed) session.
13429
- */
13430
- async getAll() {
13431
- if (!this.db) await this.initialize();
13432
- if (!this.db) return [];
13433
- return new Promise((resolve) => {
13434
- const req = this.db.transaction([STORE_NAME], "readonly").objectStore(STORE_NAME).getAll();
13435
- req.onsuccess = () => resolve(req.result || []);
13436
- req.onerror = () => {
13437
- logger.warn("[event-store] getAll failed");
13438
- resolve([]);
13439
- };
13440
- });
13441
- }
13442
- };
13443
- const eventStore = new EventStore();
13444
- //#endregion
13445
13363
  //#region node_modules/.pnpm/@opentelemetry+exporter-metrics-otlp-http@0.218.0_@opentelemetry+api@1.9.1/node_modules/@opentelemetry/exporter-metrics-otlp-http/build/esm/OTLPMetricExporterOptions.js
13446
13364
  var AggregationTemporalityPreference;
13447
13365
  (function(AggregationTemporalityPreference) {
@@ -13550,7 +13468,7 @@ const HTTP_NETWORK_BUCKETS_MS = [
13550
13468
  5e3
13551
13469
  ];
13552
13470
  /**
13553
- * RTC playback 指标的桶。默认桶(0/5/10/25/50/…/10000)对这批指标毫无分辨率:
13471
+ * 播放指标的桶。默认桶(0/5/10/25/50/…/10000)对这批指标毫无分辨率:
13554
13472
  * 帧率只可能落在 0~25,缺帧数和卡顿次数几乎全部挤在第一个桶里,分位数因此毫无意义。
13555
13473
  * 桶按各指标的实际值域给:
13556
13474
  *
@@ -13559,9 +13477,18 @@ const HTTP_NETWORK_BUCKETS_MS = [
13559
13477
  * - 卡顿次数:数的是次数不是帧,自成一组。
13560
13478
  * - 百分比:每 5% 一档,0~100。
13561
13479
  * - 时长:分两组——整轮播放是秒级,单次卡顿是一帧起步的毫秒级。
13480
+ * - 延迟:音频送入是等距(要看偏离整秒多少),动画到达是等比(两种用法差一个数量级)。
13481
+ * - GPU 耗时:个位数毫秒,按帧预算设档。
13562
13482
  *
13563
- * 这些 metric @spatius/avatarkit-rtc 发出(meter 名 spatius-avatarkit-rtc),
13564
- * view 只能配在创建 MeterProvider 的这一侧,所以桶定义留在这里。
13483
+ * SDK @spatius/avatarkit-rtcmeter 名 spatius-avatarkit-rtc)两边的指标都在这里配:
13484
+ * view 只能配在创建 MeterProvider 的这一侧,rtc 那批虽由另一个包发出,桶定义也得留在此处。
13485
+ * 同名概念两边共用一组桶,好让两条链路的分布能直接叠着比。
13486
+ */
13487
+ /**
13488
+ * 上界给到 30 而非 25:25fps 是目标帧率,正常轮次全落在那一档,若把它当上界,
13489
+ * 每一轮健康播放都会挤进 +Inf 溢出桶,与真正超发的异常轮次混在一起,
13490
+ * 这条 metric 就只剩「是否达标」这一个比特的信息。25 单独成档、上面再留几格,
13491
+ * 达标与超发才分得开。
13565
13492
  */
13566
13493
  const RTC_FPS_BUCKETS = [
13567
13494
  0,
@@ -13588,7 +13515,12 @@ const RTC_FPS_BUCKETS = [
13588
13515
  21,
13589
13516
  22,
13590
13517
  23,
13591
- 24
13518
+ 24,
13519
+ 25,
13520
+ 26,
13521
+ 27,
13522
+ 28,
13523
+ 30
13592
13524
  ];
13593
13525
  /**
13594
13526
  * 一轮播出的帧数。档位是播放时长桶除以帧间隔(40ms),和时长视角一一对应:
@@ -13690,8 +13622,120 @@ const RTC_STALL_DURATION_BUCKETS_MS = [
13690
13622
  5120,
13691
13623
  10240
13692
13624
  ];
13625
+ /**
13626
+ * 音频送入节奏:tap_N 是第 N 秒音频送入的时刻,metric 记的是相对首包(tap_0)的累计 Δ。
13627
+ * 流式输入下 tap_1/2/4 天然聚在 1000/2000/4000 附近,偏离多少正是要看的——喂得比实时慢,
13628
+ * 后台就攒不够音频、推理迟迟不触发。等距 200ms 让这三个中心各自落在档位边界上,
13629
+ * 偏移一两档就能读出来;等比会把它们挤进同一格。
13630
+ *
13631
+ * 上界 6000 而非 4000:4000 是 tap_4 的理想值,拿理想值当上界,正常轮次会全部挤进
13632
+ * +Inf 溢出桶,与真正喂得慢的轮次混在一起。
13633
+ */
13634
+ const AUDIO_LATENCY_BUCKETS_MS = [
13635
+ 0,
13636
+ 200,
13637
+ 400,
13638
+ 600,
13639
+ 800,
13640
+ 1e3,
13641
+ 1200,
13642
+ 1400,
13643
+ 1600,
13644
+ 1800,
13645
+ 2e3,
13646
+ 2200,
13647
+ 2400,
13648
+ 2600,
13649
+ 2800,
13650
+ 3e3,
13651
+ 3200,
13652
+ 3400,
13653
+ 3600,
13654
+ 3800,
13655
+ 4e3,
13656
+ 4200,
13657
+ 4400,
13658
+ 4600,
13659
+ 4800,
13660
+ 5e3,
13661
+ 5200,
13662
+ 5400,
13663
+ 5600,
13664
+ 5800,
13665
+ 6e3
13666
+ ];
13667
+ /**
13668
+ * 动画包到达:anim_N 是第 N 组动画包到达的时刻(同样相对 tap_0 的累计 Δ)。
13669
+ * 后台攒够音频(2s/4s/4s)就推理下发,所以这个值等于「攒音频的等待 + 推理 + 网络下行」,
13670
+ * 与播放进度无关:一次性灌入 10s 预制音频,三个包会连着回来,全落在低位;实时流式喂,
13671
+ * 则要等音频攒够,anim_2 可以到十几秒。两种用法差一个数量级,同一个直方图要都装得下,
13672
+ * 因此用等比而非等距——低位密到看清推理耗时,高位疏到兜住慢速流式。
13673
+ *
13674
+ * 200ms 起步:网络下行本身就要这个量级,再往下细分没有意义。
13675
+ */
13676
+ const ANIM_LATENCY_BUCKETS_MS = [
13677
+ 0,
13678
+ 200,
13679
+ 400,
13680
+ 800,
13681
+ 1600,
13682
+ 3200,
13683
+ 6400,
13684
+ 12800,
13685
+ 25600
13686
+ ];
13687
+ /**
13688
+ * 单帧 GPU 渲染耗时。实测中低端机器可以到 30ms 上下,因此值域按机型跨度给到 100ms,
13689
+ * 每 5ms 一档——而不是只覆盖高端机那几毫秒。40ms 是一帧的总预算,越过就意味着 GPU
13690
+ * 单独一项已经吃满整帧;再往上到 100ms 是给最差的机型留的观测空间。
13691
+ *
13692
+ * webgl 没有可移植的 GPU timer,取不到样本,那几轮整条 metric 不发(见 AvatarController),
13693
+ * 所以这里的分布只含 webgpu 的真实数据,不会被一堆 0 压在第一格。
13694
+ */
13695
+ const GPU_RENDER_BUCKETS_MS = [
13696
+ 5,
13697
+ 10,
13698
+ 15,
13699
+ 20,
13700
+ 25,
13701
+ 30,
13702
+ 35,
13703
+ 40,
13704
+ 45,
13705
+ 50,
13706
+ 55,
13707
+ 60,
13708
+ 65,
13709
+ 70,
13710
+ 75,
13711
+ 80,
13712
+ 85,
13713
+ 90,
13714
+ 95,
13715
+ 100
13716
+ ];
13693
13717
  /** metric 名 → 桶,供下面的 views 展开。 */
13694
- const RTC_BUCKET_PROFILES = [
13718
+ const BUCKET_PROFILES = [
13719
+ ["playback_avg_fps", RTC_FPS_BUCKETS],
13720
+ ["playback_duration_ms", RTC_PLAYBACK_DURATION_BUCKETS_MS],
13721
+ ["playback_audio_duration_ms", RTC_PLAYBACK_DURATION_BUCKETS_MS],
13722
+ ["playback_animation_frame_count", RTC_FRAME_COUNT_BUCKETS],
13723
+ ["playback_jank_frame_count", RTC_LOST_FRAME_BUCKETS],
13724
+ ["playback_stall_count", RTC_STALL_COUNT_BUCKETS],
13725
+ ["playback_stall_max_ms", RTC_STALL_DURATION_BUCKETS_MS],
13726
+ ["playback_stall_total_ms", RTC_STALL_DURATION_BUCKETS_MS],
13727
+ ["playback_audio_stall_count", RTC_STALL_COUNT_BUCKETS],
13728
+ ["playback_audio_stall_max_ms", RTC_STALL_DURATION_BUCKETS_MS],
13729
+ ["playback_audio_stall_total_ms", RTC_STALL_DURATION_BUCKETS_MS],
13730
+ ["playback_gpu_render_p50_ms", GPU_RENDER_BUCKETS_MS],
13731
+ ["playback_gpu_render_p90_ms", GPU_RENDER_BUCKETS_MS],
13732
+ ["playback_gpu_render_p95_ms", GPU_RENDER_BUCKETS_MS],
13733
+ ["playback_latency_tap_1_ms", AUDIO_LATENCY_BUCKETS_MS],
13734
+ ["playback_latency_tap_2_ms", AUDIO_LATENCY_BUCKETS_MS],
13735
+ ["playback_latency_tap_4_ms", AUDIO_LATENCY_BUCKETS_MS],
13736
+ ["playback_latency_anim_0_ms", ANIM_LATENCY_BUCKETS_MS],
13737
+ ["playback_latency_anim_1_ms", ANIM_LATENCY_BUCKETS_MS],
13738
+ ["playback_latency_anim_2_ms", ANIM_LATENCY_BUCKETS_MS],
13695
13739
  ["rtc_playback_avg_fps", RTC_FPS_BUCKETS],
13696
13740
  ["rtc_playback_frame_count", RTC_FRAME_COUNT_BUCKETS],
13697
13741
  ["rtc_playback_skipped_frames", RTC_LOST_FRAME_BUCKETS],
@@ -13700,13 +13744,49 @@ const RTC_BUCKET_PROFILES = [
13700
13744
  ["rtc_playback_start_transition_missing", RTC_LOST_FRAME_BUCKETS],
13701
13745
  ["rtc_playback_end_transition_missing", RTC_LOST_FRAME_BUCKETS],
13702
13746
  ["rtc_playback_skip_rate_pct", RTC_PERCENT_BUCKETS],
13747
+ ["rtc_playback_stall_rate_pct", RTC_PERCENT_BUCKETS],
13703
13748
  ["rtc_playback_duration_ms", RTC_PLAYBACK_DURATION_BUCKETS_MS],
13704
13749
  ["rtc_playback_stall_total_ms", RTC_STALL_DURATION_BUCKETS_MS],
13705
13750
  ["rtc_playback_stall_max_ms", RTC_STALL_DURATION_BUCKETS_MS],
13706
13751
  ["rtc_transport_packets_lost", RTC_LOST_FRAME_BUCKETS],
13707
13752
  ["rtc_transport_packets_recovered", RTC_LOST_FRAME_BUCKETS],
13708
13753
  ["rtc_transport_packets_dropped", RTC_LOST_FRAME_BUCKETS],
13709
- ["rtc_transport_packets_out_of_order", RTC_LOST_FRAME_BUCKETS]
13754
+ ["rtc_transport_packets_out_of_order", RTC_LOST_FRAME_BUCKETS],
13755
+ ["rtc_transport_rtp_packets_lost", RTC_LOST_FRAME_BUCKETS],
13756
+ ["rtc_transport_rtp_loss_rate_pct", RTC_PERCENT_BUCKETS],
13757
+ ["rtc_transport_rtt_ms", [
13758
+ 10,
13759
+ 20,
13760
+ 30,
13761
+ 40,
13762
+ 50,
13763
+ 75,
13764
+ 100,
13765
+ 150,
13766
+ 200,
13767
+ 300,
13768
+ 400,
13769
+ 600,
13770
+ 800,
13771
+ 1e3
13772
+ ]],
13773
+ ["rtc_transport_jitter_ms", [
13774
+ 1,
13775
+ 2,
13776
+ 3,
13777
+ 5,
13778
+ 8,
13779
+ 12,
13780
+ 16,
13781
+ 20,
13782
+ 25,
13783
+ 30,
13784
+ 40,
13785
+ 60,
13786
+ 80,
13787
+ 120,
13788
+ 200
13789
+ ]]
13710
13790
  ];
13711
13791
  const METRIC_EXPORT_INTERVAL_MS = 1e4;
13712
13792
  let sdkVersion$2 = "1.0.0";
@@ -13740,11 +13820,11 @@ function initializeOtelMetrics(version, resourceAttrs) {
13740
13820
  "dsm": resourceAttrs.dsm
13741
13821
  }),
13742
13822
  readers: [new PeriodicExportingMetricReader({
13743
- exporter: new OTLPMetricExporter({
13823
+ exporter: observeExporter(new OTLPMetricExporter({
13744
13824
  url: OTEL_METRICS_ENDPOINT,
13745
13825
  headers: { "Authorization": buildBasicAuthHeader$1() },
13746
13826
  temporalityPreference: AggregationTemporality.DELTA
13747
- }),
13827
+ }), "/v1/metrics", OTEL_METRICS_ENDPOINT),
13748
13828
  exportIntervalMillis: METRIC_EXPORT_INTERVAL_MS
13749
13829
  })],
13750
13830
  views: [{
@@ -13753,7 +13833,7 @@ function initializeOtelMetrics(version, resourceAttrs) {
13753
13833
  type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
13754
13834
  options: { boundaries: HTTP_NETWORK_BUCKETS_MS }
13755
13835
  }
13756
- }, ...RTC_BUCKET_PROFILES.map(([instrumentName, boundaries]) => ({
13836
+ }, ...BUCKET_PROFILES.map(([instrumentName, boundaries]) => ({
13757
13837
  instrumentName,
13758
13838
  aggregation: {
13759
13839
  type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM,
@@ -13781,6 +13861,23 @@ function getHistogram(name) {
13781
13861
  return h;
13782
13862
  }
13783
13863
  /**
13864
+ * 本周期内是否写入过**业务** metric。
13865
+ *
13866
+ * 观测遥测通道自身的那条记录(http.client.request.duration 的 /v1/* operation)有意
13867
+ * 不弄脏它:那条记录本身产生于「导出」这个动作,若也算数,就成了
13868
+ * 导出 → 写入 → 下周期非空 → 又导出 → 又写入 的永动机,用户什么都不做也每 10s 发一包。
13869
+ *
13870
+ * 只有真实业务 metric 才把它置真;metrics 导出前若为假就整轮跳过,观测记录攒着等下一趟
13871
+ * 真有业务数据的车捎走。
13872
+ */
13873
+ let hasBusinessMetric = false;
13874
+ /** 供导出侧读取并复位。@internal */
13875
+ function takeBusinessMetricFlag() {
13876
+ const v = hasBusinessMetric;
13877
+ hasBusinessMetric = false;
13878
+ return v;
13879
+ }
13880
+ /**
13784
13881
  * 记录一个 metric 数据点(Histogram)。
13785
13882
  * @param name metric 名
13786
13883
  * @param value 数值(延迟 ms / 帧率 / 时长 / 次数 / 比率等)
@@ -13793,6 +13890,7 @@ function recordMetric(name, value, attributes = {}) {
13793
13890
  const h = getHistogram(name);
13794
13891
  if (!h) return;
13795
13892
  try {
13893
+ if (name !== HTTP_CLIENT_DURATION_METRIC || !String(attributes.operation ?? "").startsWith("/v1/")) hasBusinessMetric = true;
13796
13894
  h.record(value, attributes);
13797
13895
  } catch (error) {
13798
13896
  logger.warn(`[OTel-Metrics] Failed to record ${name}:`, error instanceof Error ? error.message : String(error));
@@ -13811,12 +13909,13 @@ function recordMetric(name, value, attributes = {}) {
13811
13909
  * @internal
13812
13910
  */
13813
13911
  function recordHttpClientDuration(params) {
13814
- const { operation, method, durationMs, statusCode, serverAddress } = params;
13912
+ const { operation, method, durationMs, statusCode, serverAddress, cacheHit } = params;
13815
13913
  const attrs = {
13816
13914
  "http.request.method": method,
13817
13915
  "operation": operation || "_OTHER"
13818
13916
  };
13819
13917
  if (serverAddress) attrs["server.address"] = serverAddress;
13918
+ if (typeof cacheHit === "boolean") attrs["cache_hit"] = cacheHit;
13820
13919
  if (typeof statusCode === "number") attrs["http.response.status_code"] = statusCode;
13821
13920
  else attrs["error.type"] = "transport_error";
13822
13921
  recordMetric(HTTP_CLIENT_DURATION_METRIC, durationMs, attrs);
@@ -13841,6 +13940,226 @@ function cleanupOtelMetrics() {
13841
13940
  }
13842
13941
  }
13843
13942
  //#endregion
13943
+ //#region utils/otel-export-observer.ts
13944
+ /**
13945
+ * 遥测通道自身的送达可观测性 —— 给 OTLP 导出请求做 Transaction 上报。
13946
+ *
13947
+ * 三条通道(logs / metrics / traces)此前把导出结果直接透传给 BatchProcessor 就丢了:
13948
+ * 导出批量失败(网络、CDN 缓存、鉴权、后端 5xx)在后台侧只表现为「某个 app_id 数据变少」,
13949
+ * 分不清是真没数据还是上报挂了。之前 EdgeOne 边缘缓存无 ACAO 头导致 CORS 全挂就是这类。
13950
+ *
13951
+ * 复用业务请求那套 `http.client.request.duration`(同 metric 名、同 Network profile 桶、
13952
+ * 同成败判定),只是新增 operation 取值。后台不必为此建新看板。
13953
+ *
13954
+ * ## 自举局限(务必知悉)
13955
+ *
13956
+ * 这条上报**走的正是它要观测的通道**,所以:
13957
+ * - 通道整体挂掉时,这条 metric 同样发不出去 —— 覆盖不了「全挂」场景。全挂在后台侧的
13958
+ * 表现是该 app_id 数据整体消失,那本身才是信号。
13959
+ * - 真正能观测到的是**部分失败**(三条通道各自独立 exporter,logs 挂了 metrics 可能还通)
13960
+ * 与**间歇失败**(断续失败后恢复,失败计数随下一批发出)。
13961
+ *
13962
+ * ## 记录时机:暂存,等下一次导出搭车
13963
+ *
13964
+ * 导出结束时**不直接写 histogram**,只记进内存里的 pending 列表;等下一次导出发生时,
13965
+ * 才在其**开始**把 pending 写进 histogram。
13966
+ *
13967
+ * 这样做是为了让 metrics 通道自己也能被观测。直接写会形成自反馈:metrics 的导出由
13968
+ * `PeriodicExportingMetricReader` 周期驱动,写入产生数据点 → 下周期必须导出 → 导出
13969
+ * 又写入 —— 空闲时也永不静默。改成暂存后,**写入本身不触发任何导出**,只是等下一次
13970
+ * 本来就要发生的导出把它带走,环就断了。
13971
+ *
13972
+ * 代价是延迟一个周期:reader 先 `collect()` 快照、再 `_export()` 发送(见
13973
+ * PeriodicExportingMetricReader 的 _runOnce),所以在导出时写入必然错过本轮快照,
13974
+ * 数据落在下一轮。对成功率这种趋势指标,慢一个周期无妨。
13975
+ *
13976
+ * 副作用是最后一批 pending 会随页面关闭丢失(进程结束,没有"下一次导出")。同样可接受:
13977
+ * 丢的是最后 10s 的成功率样本,不是业务数据。
13978
+ *
13979
+ * @internal
13980
+ */
13981
+ /**
13982
+ * 已完成但尚未写进 histogram 的导出结果。上限用于兜底:正常情况下每个周期都会被
13983
+ * 排空,只有在「导出彻底停止但仍有 record 进来」这类异常下才会堆积。
13984
+ */
13985
+ const pending = [];
13986
+ const PENDING_LIMIT = 256;
13987
+ /** 暂存一次 OTLP 导出的结果,等下一次导出搭车写入(见文件头「记录时机」)。 */
13988
+ function enqueueExportResult(p) {
13989
+ if (pending.length >= PENDING_LIMIT) pending.shift();
13990
+ pending.push(p);
13991
+ }
13992
+ /**
13993
+ * 把暂存的导出结果写进 histogram。在**导出开始时**调用:此时 reader 已完成本轮
13994
+ * collect 快照,写入的数据会随下一轮发出。
13995
+ */
13996
+ function flushPendingExports() {
13997
+ if (pending.length === 0) return;
13998
+ const batch = pending.splice(0, pending.length);
13999
+ for (const p of batch) recordHttpClientDuration({
14000
+ operation: p.channel,
14001
+ method: "POST",
14002
+ durationMs: p.durationMs,
14003
+ statusCode: p.success ? 200 : void 0,
14004
+ serverAddress: p.serverAddress
14005
+ });
14006
+ }
14007
+ /** 从 OTLP endpoint URL 取 host(低基数),取不到时退回 `_OTHER`。 */
14008
+ function hostOf(endpoint) {
14009
+ try {
14010
+ return new URL(endpoint).host;
14011
+ } catch {
14012
+ return "_OTHER";
14013
+ }
14014
+ }
14015
+ /**
14016
+ * 给任意 OTLP exporter 包一层导出结果观测。原样透传参数与 callback,只在 callback
14017
+ * 回来时把结果暂存 —— 不改变导出行为,也不吞异常。
14018
+ */
14019
+ function observeExporter(inner, channel, endpoint) {
14020
+ const serverAddress = hostOf(endpoint);
14021
+ const wrapped = new Proxy(inner, { get(target, prop, receiver) {
14022
+ if (prop === "export") return observedExport;
14023
+ const value = Reflect.get(target, prop, receiver);
14024
+ return typeof value === "function" ? value.bind(target) : value;
14025
+ } });
14026
+ const observedExport = function(items, resultCallback) {
14027
+ try {
14028
+ flushPendingExports();
14029
+ } catch {}
14030
+ if (channel === "/v1/metrics" && !takeBusinessMetricFlag()) {
14031
+ resultCallback({ code: 0 });
14032
+ return;
14033
+ }
14034
+ const startMs = Date.now();
14035
+ inner.export(items, (result) => {
14036
+ try {
14037
+ enqueueExportResult({
14038
+ channel,
14039
+ durationMs: Math.round(Date.now() - startMs),
14040
+ success: result.code === 0,
14041
+ serverAddress
14042
+ });
14043
+ } catch {}
14044
+ resultCallback(result);
14045
+ });
14046
+ };
14047
+ return wrapped;
14048
+ }
14049
+ //#endregion
14050
+ //#region utils/event-store.ts
14051
+ /**
14052
+ * Event Store - Persistent buffer for OTel telemetry events.
14053
+ *
14054
+ * Each emitted event is written here before being handed to the OTel
14055
+ * BatchProcessor. When the wrapper exporter picks a batch up for HTTP
14056
+ * upload it deletes those records — so anything still here on next
14057
+ * startup is unsent and gets re-emitted (replay).
14058
+ *
14059
+ * Schema is intentionally minimal: no status / retry_count. A record's
14060
+ * presence in the store means "not yet handed to inner.export".
14061
+ * @internal
14062
+ */
14063
+ const DB_NAME = "avatarkit_events";
14064
+ const STORE_NAME = "events";
14065
+ const DB_VERSION = 1;
14066
+ var EventStore = class {
14067
+ db = null;
14068
+ initPromise = null;
14069
+ initialize() {
14070
+ if (this.db) return Promise.resolve();
14071
+ if (this.initPromise) return this.initPromise;
14072
+ this.initPromise = new Promise((resolve, reject) => {
14073
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
14074
+ req.onerror = () => reject(/* @__PURE__ */ new Error("Failed to open avatarkit_events"));
14075
+ req.onsuccess = () => {
14076
+ this.db = req.result;
14077
+ resolve();
14078
+ };
14079
+ req.onupgradeneeded = (e) => {
14080
+ const db = e.target.result;
14081
+ if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, {
14082
+ keyPath: "id",
14083
+ autoIncrement: true
14084
+ });
14085
+ };
14086
+ });
14087
+ return this.initPromise;
14088
+ }
14089
+ /**
14090
+ * Append a record, returning the auto-generated id.
14091
+ * Caller must use this id as the `_index` attribute when emitting to OTel.
14092
+ */
14093
+ async add(record) {
14094
+ if (!this.db) await this.initialize();
14095
+ if (!this.db) throw new Error("event-store not available");
14096
+ return new Promise((resolve, reject) => {
14097
+ const req = this.db.transaction([STORE_NAME], "readwrite").objectStore(STORE_NAME).add(record);
14098
+ req.onsuccess = () => resolve(req.result);
14099
+ req.onerror = () => reject(/* @__PURE__ */ new Error("event-store add failed"));
14100
+ });
14101
+ }
14102
+ /**
14103
+ * Atomically: for each id, check whether it still exists in the store;
14104
+ * if so, delete it and include it in the returned set. Used by the
14105
+ * wrapper exporter to claim ownership of a batch — only records the
14106
+ * caller successfully "took" should be forwarded to inner.export.
14107
+ *
14108
+ * This is the multi-tab dedup mechanism: when Tab B starts and replays
14109
+ * records that Tab A also still has in memory, only one tab will win
14110
+ * the take and actually send.
14111
+ *
14112
+ * The get + delete pair runs inside a single readwrite transaction so
14113
+ * concurrent tabs see consistent state.
14114
+ */
14115
+ async takeIfExists(ids) {
14116
+ const taken = /* @__PURE__ */ new Set();
14117
+ if (ids.length === 0) return taken;
14118
+ if (!this.db) await this.initialize();
14119
+ if (!this.db) return taken;
14120
+ return new Promise((resolve) => {
14121
+ const tx = this.db.transaction([STORE_NAME], "readwrite");
14122
+ const store = tx.objectStore(STORE_NAME);
14123
+ let pending = ids.length;
14124
+ const finish = () => {
14125
+ if (--pending === 0) resolve(taken);
14126
+ };
14127
+ for (const id of ids) {
14128
+ const req = store.get(id);
14129
+ req.onsuccess = () => {
14130
+ if (req.result !== void 0) {
14131
+ taken.add(id);
14132
+ store.delete(id);
14133
+ }
14134
+ finish();
14135
+ };
14136
+ req.onerror = () => finish();
14137
+ }
14138
+ tx.onerror = () => {
14139
+ logger.warn("[event-store] takeIfExists tx failed");
14140
+ resolve(taken);
14141
+ };
14142
+ });
14143
+ }
14144
+ /**
14145
+ * Return all stored records. Used on startup to replay anything left
14146
+ * over from a previous (possibly crashed) session.
14147
+ */
14148
+ async getAll() {
14149
+ if (!this.db) await this.initialize();
14150
+ if (!this.db) return [];
14151
+ return new Promise((resolve) => {
14152
+ const req = this.db.transaction([STORE_NAME], "readonly").objectStore(STORE_NAME).getAll();
14153
+ req.onsuccess = () => resolve(req.result || []);
14154
+ req.onerror = () => {
14155
+ logger.warn("[event-store] getAll failed");
14156
+ resolve([]);
14157
+ };
14158
+ });
14159
+ }
14160
+ };
14161
+ const eventStore = new EventStore();
14162
+ //#endregion
13844
14163
  //#region utils/bootstrap.ts
13845
14164
  /**
13846
14165
  * Bootstrap 全球接入入口
@@ -14230,13 +14549,13 @@ function initializeOtel(version, resourceAttrs) {
14230
14549
  "region": resourceAttrs.region,
14231
14550
  "dsm": resourceAttrs.dsm
14232
14551
  }),
14233
- processors: [new BatchLogRecordProcessor(new ReliableExporter(new OTLPLogExporter({
14552
+ processors: [new BatchLogRecordProcessor(new ReliableExporter(observeExporter(new OTLPLogExporter({
14234
14553
  url: OTEL_LOGS_ENDPOINT,
14235
14554
  headers: {
14236
14555
  "Authorization": buildBasicAuthHeader(),
14237
14556
  "stream-name": OTEL_STREAM_NAME
14238
14557
  }
14239
- })))]
14558
+ }), "/v1/logs", OTEL_LOGS_ENDPOINT)))]
14240
14559
  });
14241
14560
  logs.setGlobalLoggerProvider(loggerProvider);
14242
14561
  isInitialized$1 = true;
@@ -15115,7 +15434,7 @@ async function writeToLocalLog(message, level, extra = {}) {
15115
15434
  try {
15116
15435
  const { shouldPersist, sanitized } = sanitizeForLocalLog(message);
15117
15436
  if (!shouldPersist) return;
15118
- const { AvatarSDK } = await import("./AvatarSDK-DR8dNylh.js").then((n) => n.n);
15437
+ const { AvatarSDK } = await import("./AvatarSDK-C_Anluwp.js").then((n) => n.n);
15119
15438
  const version = AvatarSDK.version;
15120
15439
  const appId = idManager.getAppId();
15121
15440
  const userId = idManager.getUserId();
@@ -15205,4 +15524,4 @@ function captureErrorContext(level, error) {
15205
15524
  });
15206
15525
  }
15207
15526
  //#endregion
15208
- export { ROOT_CONTEXT as $, hrTimeDuration as A, getStringFromEnv as B, OTLPExporterBase as C, ExportResultCode as D, BindOnceFuture as E, ATTR_EXCEPTION_MESSAGE as F, diag as G, isTracingSuppressed as H, ATTR_EXCEPTION_STACKTRACE as I, isValidTraceId as J, context as K, ATTR_EXCEPTION_TYPE as L, isTimeInputHrTime as M, millisToHrTime as N, addHrTimes as O, otperformance as P, createNoopMeter as Q, ATTR_SERVICE_NAME as R, toAttributes$1 as S, resourceFromAttributes as T, suppressTracing as U, globalErrorHandler as V, trace as W, TraceFlags as X, INVALID_SPAN_CONTEXT as Y, ContextAPI as Z, recordHttpClientDuration as _, DrivingServiceMode as _t, clientContextFields as a, OTEL_LOGS_ENDPOINT as at, createInstrumentationScope as b, logEvent as c, OTEL_TRACES_ENDPOINT as ct, cleanupOtel as d, isDebugMode as dt, createContextKey as et, initializeOtel as f, idManager as ft, initializeOtelMetrics as g, DEFAULT_REGION_REQUEST as gt, cleanupOtelMetrics as h, DEFAULT_REGION as ht, cleanupPostHog as i, unregisterGlobal as it, isTimeInput as j, hrTime as k, logMetric as l, OTEL_TRACES_STREAM_NAME as lt, fetchBootstrap as m, AvatarError as mt, setLogLevel as n, getGlobal as nt, getLogsFeatureFlag as o, OTEL_PASSWORD as ot, clockSync as p, generateTraceId as pt, isSpanContextValid as q, logSink as r, registerGlobal as rt, initializePostHog as s, OTEL_STREAM_NAME as st, logger as t, DiagAPI as tt, updatePostHogPersonPropertiesForFlags as u, OTEL_USERNAME as ut, createLegacyOtlpBrowserExportDelegate as v, ErrorCode as vt, defaultResource as w, createResource as x, JSON_ENCODER as y, LogLevel as yt, getNumberFromEnv as z };
15527
+ export { ContextAPI as $, addHrTimes as A, ATTR_SERVICE_NAME as B, createResource as C, resourceFromAttributes as D, defaultResource as E, millisToHrTime as F, suppressTracing as G, getStringFromEnv as H, otperformance as I, context as J, trace as K, ATTR_EXCEPTION_MESSAGE as L, hrTimeDuration as M, isTimeInput as N, BindOnceFuture as O, isTimeInputHrTime as P, TraceFlags as Q, ATTR_EXCEPTION_STACKTRACE as R, createInstrumentationScope as S, OTLPExporterBase as T, globalErrorHandler as U, getNumberFromEnv as V, isTracingSuppressed as W, isValidTraceId as X, isSpanContextValid as Y, INVALID_SPAN_CONTEXT as Z, cleanupOtelMetrics as _, DEFAULT_REGION as _t, clientContextFields as a, registerGlobal as at, createLegacyOtlpBrowserExportDelegate as b, ErrorCode as bt, logEvent as c, OTEL_PASSWORD as ct, cleanupOtel as d, OTEL_TRACES_STREAM_NAME as dt, createNoopMeter as et, initializeOtel as f, OTEL_USERNAME as ft, observeExporter as g, AvatarError as gt, hostOf as h, generateTraceId as ht, cleanupPostHog as i, getGlobal as it, hrTime as j, ExportResultCode as k, logMetric as l, OTEL_STREAM_NAME as lt, fetchBootstrap as m, idManager as mt, setLogLevel as n, createContextKey as nt, getLogsFeatureFlag as o, unregisterGlobal as ot, clockSync as p, isDebugMode as pt, diag as q, logSink as r, DiagAPI as rt, initializePostHog as s, OTEL_LOGS_ENDPOINT as st, logger as t, ROOT_CONTEXT as tt, updatePostHogPersonPropertiesForFlags as u, OTEL_TRACES_ENDPOINT as ut, initializeOtelMetrics as v, DEFAULT_REGION_REQUEST as vt, toAttributes$1 as w, JSON_ENCODER as x, LogLevel as xt, recordHttpClientDuration as y, DrivingServiceMode as yt, ATTR_EXCEPTION_TYPE as z };