@xmov/avatar 2.0.0-alpha.61 → 2.0.0-alpha.63

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.
@@ -0,0 +1,58 @@
1
+ import type { SDKError } from 'types/error';
2
+ export interface SentryReporterOptions {
3
+ /** Traces 采样率,默认 1.0(100% 全量) */
4
+ tracesSampleRate?: number;
5
+ }
6
+ /** span 附加属性(兼容 Sentry SpanAttributeValue 的标量子集) */
7
+ type SpanData = Record<string, string | number | boolean | undefined>;
8
+ /**
9
+ * Sentry 错误上报器(隔离实例)
10
+ *
11
+ * 使用独立的 BrowserClient + Scope,不污染宿主应用的全局 Sentry 状态。
12
+ * 此文件仅在公有云构建(ENABLE_SENTRY=true)时被打包,
13
+ * 私有化构建时通过编译时常量 + 死代码消除完全移除。
14
+ */
15
+ export declare class SentryReporter {
16
+ private client;
17
+ private scope;
18
+ private tracesSampleRate;
19
+ constructor(dsn: string, options?: SentryReporterOptions);
20
+ /**
21
+ * 上报 SDK 错误
22
+ */
23
+ captureError(error: SDKError): void;
24
+ /**
25
+ * 在隔离 scope 上启动一个 span 并执行回调,回调返回时自动结束。
26
+ * 异常时 span 自动标记 error(由 startSpan 内部处理),span 数据发往本实例的 DSN。
27
+ */
28
+ startSpan<T>(name: string, ctx: {
29
+ op?: string;
30
+ data?: SpanData;
31
+ }, fn: () => T): T;
32
+ /**
33
+ * 启动一个手动管理生命周期的 span(跨回调/异步时用),
34
+ * 返回的句柄在操作完成时调用 end() 结束 span。
35
+ */
36
+ startInactiveSpan(name: string, ctx: {
37
+ op?: string;
38
+ data?: SpanData;
39
+ }): {
40
+ end(): void;
41
+ };
42
+ /**
43
+ * 设置全局标签
44
+ */
45
+ setTag(key: string, value: string): void;
46
+ /**
47
+ * 设置用户信息
48
+ */
49
+ setUser(user: {
50
+ id?: string;
51
+ ip_address?: string;
52
+ }): void;
53
+ /**
54
+ * 销毁 Sentry 客户端,flush 未发送的事件
55
+ */
56
+ destroy(): Promise<void>;
57
+ }
58
+ export {};
@@ -96,9 +96,18 @@ export interface IAvatarOptions {
96
96
  */
97
97
  onAudioPlaybackData?(data: IAudioPlaybackData): void;
98
98
  /**
99
- * 每帧数据回调
100
- * 返回当前帧的 face + body 原始数据(帧号、body_id 等)
99
+ * Sentry 错误监控配置
100
+ * 仅在公有云构建(ENABLE_SENTRY=true)时生效,私有化构建会忽略此配置
101
+ * 设为 { enabled: false } 可禁用 Sentry 上报
101
102
  */
103
+ sentry?: {
104
+ enabled?: boolean;
105
+ /**
106
+ * 性能 Traces 采样率(0-1),默认 1.0(100% 全量)
107
+ * 容量受限时可下调,如 0.1(10%)
108
+ */
109
+ tracesSampleRate?: number;
110
+ };
102
111
  onFrameDataUpdate?(data: IFrameDataUpdate): void;
103
112
  }
104
113
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmov/avatar",
3
- "version": "2.0.0-alpha.61",
3
+ "version": "2.0.0-alpha.63",
4
4
  "description": "魔珐数字人 JS-SDK",
5
5
  "type": "module",
6
6
  "source": "src/index.ts",
@@ -43,13 +43,14 @@
43
43
  "!dist/agent/microphone.d.ts"
44
44
  ],
45
45
  "scripts": {
46
- "build": "pnpm run build:core && pnpm run build:agent && pnpm run build:e2e:cdn && pnpm run test:e2e:cdn && node upload-sdk.js build",
47
- "build:core": "microbundle --workers --define ENV=production,VERSION=$npm_package_version --external none --tsconfig tsconfig.json",
48
- "build:agent": "microbundle --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json --no-sourcemap && node scripts/build-agent-entry.mjs",
46
+ "build": "pnpm run build:core && pnpm run build:agent && pnpm run build:e2e:cdn && pnpm run test:e2e:cdn && node scripts/upload-sourcemaps.mjs && node upload-sdk.js build",
47
+ "build:core": "microbundle --workers --define ENV=production,VERSION=$npm_package_version,ENABLE_SENTRY=true,SENTRY_DSN='https://aabed165326fc7a2a276b49aa4043be3@xmovsentry.xmov.ai/32' --external none --tsconfig tsconfig.json",
48
+ "build:agent": "microbundle --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json --no-sourcemap --define ENV=production,VERSION=$npm_package_version,ENABLE_SENTRY=true,SENTRY_DSN='https://aabed165326fc7a2a276b49aa4043be3@xmovsentry.xmov.ai/32' && node scripts/build-agent-entry.mjs",
49
+ "build:private": "microbundle --workers --define ENV=production,VERSION=$npm_package_version,ENABLE_SENTRY=false,SENTRY_DSN='' --external @sentry/browser --tsconfig tsconfig.json && node scripts/strip-sentry-imports.mjs && pnpm run build:agent",
49
50
  "build:e2e:cdn": "microbundle --entry src/agent/cdn-entry.ts --output cdn-dist/xmovAvatar_e2e.js --format iife --external none --workers --define ENV=production,VERSION=$npm_package_version --tsconfig tsconfig.agent.json --no-pkg-main --no-sourcemap",
50
51
  "sdk": "pnpm --filter @xmov/avatar run --parallel --no-bail \"/^sdk:.+$/\"",
51
- "sdk:core": "microbundle watch --workers --define ENV=development,VERSION=$npm_package_version --external none --tsconfig tsconfig.json",
52
- "sdk:agent": "node scripts/build-agent-entry.mjs && microbundle watch --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json",
52
+ "sdk:core": "microbundle watch --workers --define ENV=development,VERSION=$npm_package_version,ENABLE_SENTRY=true,SENTRY_DSN='https://aabed165326fc7a2a276b49aa4043be3@xmovsentry.xmov.ai/32' --external none --tsconfig tsconfig.json",
53
+ "sdk:agent": "node scripts/build-agent-entry.mjs && microbundle watch --entry src/agent/index.ts --output dist/agent --format modern,esm,cjs,umd --external @xmov/avatar --alias ../index=@xmov/avatar --globals @xmov/avatar=XmovAvatar --name XingyunAvatarAgentModule --tsconfig tsconfig.agent.json --define ENV=development,VERSION=$npm_package_version,ENABLE_SENTRY=true,SENTRY_DSN='https://aabed165326fc7a2a276b49aa4043be3@xmovsentry.xmov.ai/32'",
53
54
  "app": "cd app/ && npm run dev",
54
55
  "app:lan": "pnpm --dir app dev:lan",
55
56
  "test:agent": "node scripts/run-agent-tests.mjs",
@@ -95,6 +96,7 @@
95
96
  "ws": "^8.21.1"
96
97
  },
97
98
  "dependencies": {
99
+ "@sentry/browser": "^10.69.0",
98
100
  "blueimp-md5": "^2.19.0",
99
101
  "protobufjs": "^7.5.4"
100
102
  },
package/src/global.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  // src/global.d.ts
2
+
3
+ // 编译时常量声明(通过 microbundle --define 注入)
4
+ declare const VERSION: string;
5
+ declare const ENV: string;
6
+ declare const ENABLE_SENTRY: boolean;
7
+ declare const SENTRY_DSN: string;
8
+
2
9
  // 1. 声明全局 protobuf 对象(适配 protobufjs UMD 格式)
3
10
  declare namespace globalThis {
4
11
  const protobuf: {
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import Ttsa from "./control/ttsa";
8
8
  import { DebugOverlay } from "./view/DebugOverlay";
9
9
  import NetworkMonitor from './modules/network';
10
10
  import Errors from './modules/error-handle';
11
+ import { SentryReporter } from './modules/sentry-reporter';
11
12
 
12
13
  import { performanceConstant } from "./utils/perfermance";
13
14
  import './utils/logger.js';
@@ -47,6 +48,9 @@ export default class XmovAvatar {
47
48
  private ttsa: Ttsa | null = null;
48
49
 
49
50
  private debugOverlay: DebugOverlay | null = null;
51
+ private sentryReporter: SentryReporter | null = null;
52
+ /** 首帧 span 句柄(init 时启动,首次 BODY 帧到达时结束) */
53
+ private firstFrameSpan: { end(): void } | null = null;
50
54
 
51
55
  private _offlineTimer = -1
52
56
  private readonly _offlineInterval = 300
@@ -570,6 +574,25 @@ export default class XmovAvatar {
570
574
  return;
571
575
  }
572
576
 
577
+ // 初始化 Sentry 错误监控(编译时常量 + 运行时配置双重控制)
578
+ // 编译时 ENABLE_SENTRY 决定代码是否打包,运行时 sentry.enabled 决定是否启用
579
+ // 提前到资源加载之前创建,使 init / resource_load span 可覆盖整个初始化过程
580
+ if (ENABLE_SENTRY && this.options.sentry?.enabled) {
581
+ try {
582
+ this.sentryReporter = new SentryReporter(SENTRY_DSN, {
583
+ tracesSampleRate: this.options.sentry?.tracesSampleRate,
584
+ });
585
+ this.sentryReporter.setTag('app_id', this.options.appId || '');
586
+ this.sentryReporter.setTag('env', this._env);
587
+ } catch (e) {
588
+ (window as any).avatarSDKLogger.warn(this.TAG, 'Sentry 初始化失败', e);
589
+ }
590
+ }
591
+ // sdk.init:初始化总耗时(含资源加载、连接就绪)
592
+ const initSpan = this.sentryReporter?.startInactiveSpan('sdk.init', { op: 'init' });
593
+ // sdk.first_frame:从初始化到首个 body 帧到达(handleMessage 首次 BODY 帧时结束)
594
+ this.firstFrameSpan = this.sentryReporter?.startInactiveSpan('sdk.first_frame', { op: 'render' }) ?? null;
595
+
573
596
  const { onDownloadProgress, initModel } = params;
574
597
  this.onDownloadProgress = (progress: number) => {
575
598
  if(progress === 15) {
@@ -586,17 +609,24 @@ export default class XmovAvatar {
586
609
  this.pendingInvisibleMode = true;
587
610
  }
588
611
 
589
- // 加载资源
612
+ // 加载资源(sdk.resource_load:资源包下载/解析耗时)
590
613
  (window as any).performanceTracker.markStart(performanceConstant.load_resource);
591
614
  (window as any).performanceTracker.markStart(performanceConstant.first_avatar_render);
592
615
  (window as any).performanceTracker.markStart(performanceConstant.first_webgl_render);
593
- const sessionInfo = await this.resourceManager.load(this.onDownloadProgress.bind(this));
616
+ const downloadProgress = this.onDownloadProgress;
617
+ const sessionInfo = this.sentryReporter
618
+ ? await this.sentryReporter.startSpan('sdk.resource_load', { op: 'resource.load' }, () =>
619
+ this.resourceManager.load(downloadProgress.bind(this))
620
+ )
621
+ : await this.resourceManager.load(downloadProgress.bind(this));
594
622
  // 如果在load过程中被销毁了,直接返回,避免继续初始化
595
623
  if(this.destroyed) {
624
+ initSpan?.end();
596
625
  (window as any).avatarSDKLogger.warn(this.TAG, "sdk destroyed during load, aborting after resourceManager load");
597
626
  return
598
627
  };
599
628
  if(!sessionInfo?.socket_io_url || !sessionInfo?.token) {
629
+ initSpan?.end();
600
630
  (window as any).avatarSDKLogger.error(this.TAG, "init error, socket_io_url or token is empty reload");
601
631
  return
602
632
  };
@@ -613,6 +643,7 @@ export default class XmovAvatar {
613
643
 
614
644
  window.addEventListener('beforeunload', this.handleBeforeUnload);
615
645
  document.addEventListener('visibilitychange', this.boundVisibilityChange);
646
+ initSpan?.end();
616
647
  return sessionInfo;
617
648
  }
618
649
 
@@ -1140,6 +1171,9 @@ export default class XmovAvatar {
1140
1171
  timestamp: Date.now(),
1141
1172
  });
1142
1173
  this.isfirstReceive = false;
1174
+ // 结束 sdk.first_frame span(首帧数据到达)
1175
+ this.firstFrameSpan?.end();
1176
+ this.firstFrameSpan = null;
1143
1177
  }
1144
1178
  // 处理下行数据
1145
1179
  this.renderScheduler.handleData(type, data);
@@ -1374,6 +1408,14 @@ export default class XmovAvatar {
1374
1408
  window.removeEventListener("beforeunload", this.handleBeforeUnload);
1375
1409
  document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1376
1410
 
1411
+ // 清理 Sentry 客户端
1412
+ if (ENABLE_SENTRY && this.sentryReporter) {
1413
+ try {
1414
+ this.sentryReporter.destroy();
1415
+ } catch {}
1416
+ this.sentryReporter = null;
1417
+ }
1418
+
1377
1419
  (window as any).avatarSDKLogger.log(this.TAG, "SDK 已销毁");
1378
1420
  }
1379
1421
 
@@ -1432,6 +1474,16 @@ export default class XmovAvatar {
1432
1474
 
1433
1475
  speak(ssml: string, is_start: boolean = true, is_end: boolean = true, extra = {client_speak_id: '', client_frame: 0}): string | undefined{
1434
1476
  if (this._isClientMode) { this._apiForwarder?.speak(ssml, is_start, is_end, extra); return; }
1477
+ if(this.sentryReporter) {
1478
+ return this.sentryReporter.startSpan('sdk.speak', {
1479
+ op: 'speak',
1480
+ data: { client_speak_id: extra.client_speak_id },
1481
+ }, () => this.speakInternal(ssml, is_start, is_end, extra));
1482
+ }
1483
+ return this.speakInternal(ssml, is_start, is_end, extra);
1484
+ }
1485
+
1486
+ private speakInternal(ssml: string, is_start: boolean = true, is_end: boolean = true, extra = {client_speak_id: '', client_frame: 0}): string | undefined{
1435
1487
  if(this.enableClientInterrupt && is_start) {
1436
1488
  this.renderScheduler.interrupt("speak");
1437
1489
  }
@@ -1578,6 +1630,15 @@ export default class XmovAvatar {
1578
1630
  if(params.code !== EErrorCode.RENDER_BODY_ERROR && params.code !== EErrorCode.RENDER_FACE_ERROR && params.code !== EErrorCode.BODY_DATA_EXPIRED) {
1579
1631
  this.options.onMessage(e)
1580
1632
  }
1633
+ // Sentry 错误上报(仅公有云构建时生效)
1634
+ if (ENABLE_SENTRY && this.sentryReporter) {
1635
+ try {
1636
+ this.sentryReporter.captureError(e);
1637
+ } catch (err) {
1638
+ (window as any).avatarSDKLogger.warn(this.TAG, 'Sentry 上报失败', err);
1639
+ }
1640
+ }
1641
+
1581
1642
  this.ttsa?.send("client_error", params);
1582
1643
  this.debugOverlay?.addError(e)
1583
1644
  }
@@ -1691,7 +1752,13 @@ export default class XmovAvatar {
1691
1752
  return;
1692
1753
  }
1693
1754
  try {
1694
- const result = await this._reload();
1755
+ // sdk.reconnect:单次重连耗时,带轮次/次数 tag
1756
+ const result = this.sentryReporter
1757
+ ? await this.sentryReporter.startSpan('sdk.reconnect', {
1758
+ op: 'reconnect',
1759
+ data: { round: this.retryRound, count: this.retryCount },
1760
+ }, () => this._reload())
1761
+ : await this._reload();
1695
1762
  if (!this.isRetrying || this.destroyed) {
1696
1763
  return;
1697
1764
  }
@@ -0,0 +1,171 @@
1
+ import {
2
+ BrowserClient,
3
+ defaultStackParser,
4
+ getDefaultIntegrations,
5
+ makeFetchTransport,
6
+ Scope,
7
+ browserTracingIntegration,
8
+ startSpan,
9
+ startInactiveSpan,
10
+ } from '@sentry/browser';
11
+
12
+ import type { SDKError } from 'types/error';
13
+
14
+ /** 渲染噪音 / 数据过期等高频低危错误码,直接过滤不上报 */
15
+ const NOISE_ERROR_CODES = ['3205', '3206', '3305', '30005'];
16
+
17
+ export interface SentryReporterOptions {
18
+ /** Traces 采样率,默认 1.0(100% 全量) */
19
+ tracesSampleRate?: number;
20
+ }
21
+
22
+ /** span 附加属性(兼容 Sentry SpanAttributeValue 的标量子集) */
23
+ type SpanData = Record<string, string | number | boolean | undefined>;
24
+
25
+ /**
26
+ * Sentry 错误上报器(隔离实例)
27
+ *
28
+ * 使用独立的 BrowserClient + Scope,不污染宿主应用的全局 Sentry 状态。
29
+ * 此文件仅在公有云构建(ENABLE_SENTRY=true)时被打包,
30
+ * 私有化构建时通过编译时常量 + 死代码消除完全移除。
31
+ */
32
+ export class SentryReporter {
33
+ private client: BrowserClient;
34
+ private scope: Scope;
35
+ private tracesSampleRate: number;
36
+
37
+ constructor(dsn: string, options: SentryReporterOptions = {}) {
38
+ this.tracesSampleRate = options.tracesSampleRate ?? 1.0;
39
+
40
+ // 过滤掉依赖全局状态的集成,避免与宿主应用的 Sentry 实例冲突
41
+ const integrations = getDefaultIntegrations({}).filter(
42
+ (i) =>
43
+ ![
44
+ 'BrowserApiErrors',
45
+ 'BrowserSession',
46
+ 'Breadcrumbs',
47
+ 'GlobalHandlers',
48
+ 'FunctionToString',
49
+ 'Dedupe',
50
+ ].includes(i.name),
51
+ );
52
+
53
+ // 显式加入 BrowserTracing(getDefaultIntegrations 未包含),
54
+ // 关闭页面加载/导航/请求自动追踪,只保留手动 startSpan 埋点,避免噪音 span
55
+ integrations.push(
56
+ browserTracingIntegration({
57
+ instrumentPageLoad: false,
58
+ instrumentNavigation: false,
59
+ traceFetch: false,
60
+ traceXHR: false,
61
+ }),
62
+ );
63
+
64
+ this.client = new BrowserClient({
65
+ dsn,
66
+ transport: makeFetchTransport,
67
+ stackParser: defaultStackParser,
68
+ integrations,
69
+ release: `xmov-avatar@${VERSION}`,
70
+ tracesSampleRate: this.tracesSampleRate,
71
+ beforeSend(event) {
72
+ // ① 白名单:仅上报 SDK 自身错误(source 标签由 scope 打标),过滤宿主页面异常
73
+ if (event.tags?.source !== 'xmov-avatar-sdk') {
74
+ return null;
75
+ }
76
+ // ② 噪音码过滤:高频渲染噪音 / 数据过期类,避免事件量过大
77
+ const errorCode = String(event.tags?.error_code ?? '');
78
+ if (NOISE_ERROR_CODES.includes(errorCode)) {
79
+ return null;
80
+ }
81
+ // ③ 指纹去重:按「错误码 + 消息前缀」合并,同码同消息收敛为一个 Issue
82
+ event.fingerprint = [
83
+ errorCode || 'unknown',
84
+ String(event.message ?? '').slice(0, 64),
85
+ ];
86
+ // ④ 上下文补全:浏览器/UA(app_id / session_id 等已在 scope 打标)
87
+ event.contexts = {
88
+ ...(event.contexts ?? {}),
89
+ browser: { name: navigator.userAgent.slice(0, 200) },
90
+ };
91
+ return event;
92
+ },
93
+ });
94
+
95
+ this.scope = new Scope();
96
+ this.scope.setClient(this.client);
97
+ this.scope.setTag('sdk_version', VERSION);
98
+ this.scope.setTag('source', 'xmov-avatar-sdk');
99
+ this.client.init();
100
+ }
101
+
102
+ /**
103
+ * 上报 SDK 错误
104
+ */
105
+ captureError(error: SDKError): void {
106
+ // 在隔离 scope 上设置本次错误的标签和上下文
107
+ this.scope.setTag('error_code', String(error.error_code));
108
+ this.scope.setTag('error_name', error.error_name);
109
+ this.scope.setTag('session_id', error.session_id);
110
+ this.scope.setExtra('code', error.code);
111
+ this.scope.setExtra('client_speak_id', error.client_speak_id);
112
+ this.scope.setExtra('timestamp', error.timestamp);
113
+ if (error.originalError) {
114
+ this.scope.setExtra('originalError', error.originalError);
115
+ }
116
+ this.scope.captureException(new Error(error.message));
117
+ }
118
+
119
+ /**
120
+ * 在隔离 scope 上启动一个 span 并执行回调,回调返回时自动结束。
121
+ * 异常时 span 自动标记 error(由 startSpan 内部处理),span 数据发往本实例的 DSN。
122
+ */
123
+ startSpan<T>(name: string, ctx: { op?: string; data?: SpanData }, fn: () => T): T {
124
+ return startSpan(
125
+ {
126
+ name,
127
+ op: ctx.op,
128
+ scope: this.scope,
129
+ attributes: ctx.data,
130
+ },
131
+ () => fn(),
132
+ );
133
+ }
134
+
135
+ /**
136
+ * 启动一个手动管理生命周期的 span(跨回调/异步时用),
137
+ * 返回的句柄在操作完成时调用 end() 结束 span。
138
+ */
139
+ startInactiveSpan(name: string, ctx: { op?: string; data?: SpanData }): { end(): void } {
140
+ const span = startInactiveSpan({
141
+ name,
142
+ op: ctx.op,
143
+ scope: this.scope,
144
+ attributes: ctx.data,
145
+ });
146
+ return {
147
+ end: () => span?.end(),
148
+ };
149
+ }
150
+
151
+ /**
152
+ * 设置全局标签
153
+ */
154
+ setTag(key: string, value: string): void {
155
+ this.scope.setTag(key, value);
156
+ }
157
+
158
+ /**
159
+ * 设置用户信息
160
+ */
161
+ setUser(user: { id?: string; ip_address?: string }): void {
162
+ this.scope.setUser(user);
163
+ }
164
+
165
+ /**
166
+ * 销毁 Sentry 客户端,flush 未发送的事件
167
+ */
168
+ async destroy(): Promise<void> {
169
+ await this.client.close(2000);
170
+ }
171
+ }
@@ -111,6 +111,19 @@ export interface IAvatarOptions {
111
111
  */
112
112
  onAudioPlaybackData?(data: IAudioPlaybackData): void;
113
113
  /**
114
+ * Sentry 错误监控配置
115
+ * 仅在公有云构建(ENABLE_SENTRY=true)时生效,私有化构建会忽略此配置
116
+ * 设为 { enabled: false } 可禁用 Sentry 上报
117
+ */
118
+ sentry?: {
119
+ enabled?: boolean
120
+ /**
121
+ * 性能 Traces 采样率(0-1),默认 1.0(100% 全量)
122
+ * 容量受限时可下调,如 0.1(10%)
123
+ */
124
+ tracesSampleRate?: number
125
+ }
126
+ /*
114
127
  * 每帧数据回调
115
128
  * 返回当前帧的 face + body 原始数据(帧号、body_id 等)
116
129
  */