@xmov/avatar 2.3.1 → 2.4.0-alpha.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.
@@ -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
  */
@@ -1,9 +1,24 @@
1
1
  import md5 from 'blueimp-md5';
2
+ import { generateFingerprint } from './index';
2
3
 
3
4
  function encodeWithMd5(s: string): string {
4
5
  return md5(s);
5
6
  }
6
7
 
8
+ /**
9
+ * 生成 request_id:用于 startSession / stopSession 的幂等与兜底销毁。
10
+ * 组成:md5(机器指纹 + 毫秒时间戳 + appId)
11
+ * 同一次 init → destroy 生命周期内始终不变,即使 session_id 尚未返回
12
+ * 也能用 request_id 让服务端销毁已占用的房间资源。
13
+ */
14
+ export function generateRequestId(appId: string): string {
15
+ const fingerprint = (() => {
16
+ try { return generateFingerprint(); } catch { return 'unknown'; }
17
+ })();
18
+ const payload = `${fingerprint}|${Date.now()}|${appId}`;
19
+ return md5(payload);
20
+ }
21
+
7
22
  function escapeNonAsciiForPythonJson(value: string): string {
8
23
  return value.replace(/[^\u0000-\u007E]/g, character => {
9
24
  return `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`;