sentry-miniapp 1.3.0 → 1.4.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/CHANGELOG.md +123 -0
- package/README.md +88 -0
- package/dist/sentry-miniapp.cjs.js +142 -39
- package/dist/sentry-miniapp.cjs.js.map +1 -1
- package/dist/sentry-miniapp.esm.js +142 -39
- package/dist/sentry-miniapp.esm.js.map +1 -1
- package/dist/sentry-miniapp.umd.js +142 -39
- package/dist/sentry-miniapp.umd.js.map +1 -1
- package/dist/types/client.d.ts +4 -6
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/integrations/index.d.ts +1 -0
- package/dist/types/integrations/index.d.ts.map +1 -1
- package/dist/types/integrations/networkbreadcrumbs.d.ts +29 -0
- package/dist/types/integrations/networkbreadcrumbs.d.ts.map +1 -0
- package/dist/types/sdk.d.ts +6 -4
- package/dist/types/sdk.d.ts.map +1 -1
- package/dist/types/transports/offlineStore.d.ts +4 -1
- package/dist/types/transports/offlineStore.d.ts.map +1 -1
- package/dist/types/types.d.ts +4 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +94 -16
- package/src/.keep +0 -0
- package/src/client.ts +203 -0
- package/src/crossPlatform.ts +407 -0
- package/src/eventbuilder.ts +291 -0
- package/src/helpers.ts +214 -0
- package/src/index.ts +86 -0
- package/src/integrations/dedupe.ts +215 -0
- package/src/integrations/globalhandlers.ts +209 -0
- package/src/integrations/httpcontext.ts +140 -0
- package/src/integrations/index.ts +10 -0
- package/src/integrations/linkederrors.ts +107 -0
- package/src/integrations/networkbreadcrumbs.ts +155 -0
- package/src/integrations/performance.ts +622 -0
- package/src/integrations/rewriteframes.ts +77 -0
- package/src/integrations/router.ts +180 -0
- package/src/integrations/system.ts +135 -0
- package/src/integrations/trycatch.ts +233 -0
- package/src/polyfills.ts +242 -0
- package/src/sdk.ts +182 -0
- package/src/transports/index.ts +3 -0
- package/src/transports/offlineStore.ts +85 -0
- package/src/transports/xhr.ts +68 -0
- package/src/types.ts +129 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { getCurrentScope } from '@sentry/core';
|
|
2
|
+
import type { Integration, IntegrationFn } from '@sentry/core';
|
|
3
|
+
import { startSpan } from '@sentry/core';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getPerformanceManager,
|
|
7
|
+
getSystemInfo,
|
|
8
|
+
sdk,
|
|
9
|
+
type PerformanceEntry,
|
|
10
|
+
type NavigationPerformanceEntry,
|
|
11
|
+
type RenderPerformanceEntry,
|
|
12
|
+
type ResourcePerformanceEntry,
|
|
13
|
+
type UserTimingPerformanceEntry,
|
|
14
|
+
type PerformanceManager,
|
|
15
|
+
type PerformanceObserver
|
|
16
|
+
} from '../crossPlatform';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Performance API 集成配置
|
|
20
|
+
*/
|
|
21
|
+
export interface PerformanceIntegrationOptions {
|
|
22
|
+
/** 是否启用导航性能监控 */
|
|
23
|
+
enableNavigation?: boolean;
|
|
24
|
+
/** 是否启用渲染性能监控 */
|
|
25
|
+
enableRender?: boolean;
|
|
26
|
+
/** 是否启用资源加载性能监控 */
|
|
27
|
+
enableResource?: boolean;
|
|
28
|
+
/** 是否启用用户自定义性能监控 */
|
|
29
|
+
enableUserTiming?: boolean;
|
|
30
|
+
/** 性能数据采样率 (0-1) */
|
|
31
|
+
sampleRate?: number;
|
|
32
|
+
/** 性能条目缓冲区大小 */
|
|
33
|
+
bufferSize?: number;
|
|
34
|
+
/** 自动上报间隔 (毫秒) */
|
|
35
|
+
reportInterval?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Performance API 集成 */
|
|
39
|
+
export class PerformanceIntegration implements Integration {
|
|
40
|
+
/**
|
|
41
|
+
* @inheritDoc
|
|
42
|
+
*/
|
|
43
|
+
public static id: string = 'PerformanceAPI';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @inheritDoc
|
|
47
|
+
*/
|
|
48
|
+
public name: string = PerformanceIntegration.id;
|
|
49
|
+
|
|
50
|
+
private _options: Required<PerformanceIntegrationOptions>;
|
|
51
|
+
private _performanceManager: PerformanceManager | null = null;
|
|
52
|
+
private _observers: PerformanceObserver[] = [];
|
|
53
|
+
private _entryBuffer: PerformanceEntry[] = [];
|
|
54
|
+
private _reportTimer: any = null;
|
|
55
|
+
|
|
56
|
+
constructor(options: PerformanceIntegrationOptions = {}) {
|
|
57
|
+
this._options = {
|
|
58
|
+
enableNavigation: true,
|
|
59
|
+
enableRender: true,
|
|
60
|
+
enableResource: true,
|
|
61
|
+
enableUserTiming: false,
|
|
62
|
+
sampleRate: 1.0,
|
|
63
|
+
bufferSize: 100,
|
|
64
|
+
reportInterval: 30000, // 30秒
|
|
65
|
+
...options,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @inheritDoc
|
|
71
|
+
*/
|
|
72
|
+
public setupOnce(): void {
|
|
73
|
+
this._initializePerformanceManager();
|
|
74
|
+
this._setupPerformanceObservers();
|
|
75
|
+
this._startAutoReporting();
|
|
76
|
+
this._addPerformanceContext();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 初始化性能管理器
|
|
81
|
+
*/
|
|
82
|
+
private _initializePerformanceManager(): void {
|
|
83
|
+
try {
|
|
84
|
+
this._performanceManager = getPerformanceManager();
|
|
85
|
+
if (!this._performanceManager) {
|
|
86
|
+
console.warn('[Sentry Performance] Performance API not available');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 设置性能监控标签
|
|
91
|
+
const scope = getCurrentScope();
|
|
92
|
+
scope.setTag('performance.api.available', true);
|
|
93
|
+
scope.setContext('performance', {
|
|
94
|
+
api_version: 'miniapp-1.0',
|
|
95
|
+
sample_rate: this._options.sampleRate,
|
|
96
|
+
buffer_size: this._options.bufferSize,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn('[Sentry Performance] Failed to initialize performance manager:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 设置性能观察者
|
|
105
|
+
*/
|
|
106
|
+
private _setupPerformanceObservers(): void {
|
|
107
|
+
if (!this._performanceManager) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const entryTypes: string[] = [];
|
|
113
|
+
|
|
114
|
+
if (this._options.enableNavigation) {
|
|
115
|
+
entryTypes.push('navigation');
|
|
116
|
+
}
|
|
117
|
+
if (this._options.enableRender) {
|
|
118
|
+
entryTypes.push('render');
|
|
119
|
+
}
|
|
120
|
+
if (this._options.enableResource) {
|
|
121
|
+
entryTypes.push('resource');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let canObserveUserTiming = this._options.enableUserTiming;
|
|
125
|
+
|
|
126
|
+
if (canObserveUserTiming) {
|
|
127
|
+
try {
|
|
128
|
+
const systemInfo = getSystemInfo();
|
|
129
|
+
|
|
130
|
+
if (systemInfo && systemInfo.platform === 'devtools') {
|
|
131
|
+
canObserveUserTiming = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (canObserveUserTiming) {
|
|
135
|
+
const globalObj: any =
|
|
136
|
+
typeof globalThis !== 'undefined'
|
|
137
|
+
? globalThis
|
|
138
|
+
: typeof window !== 'undefined'
|
|
139
|
+
? window
|
|
140
|
+
: {};
|
|
141
|
+
|
|
142
|
+
const performanceObserverCtor: any = (globalObj as any).PerformanceObserver;
|
|
143
|
+
const supportedTypes: string[] | undefined =
|
|
144
|
+
performanceObserverCtor && Array.isArray((performanceObserverCtor as any).supportedEntryTypes)
|
|
145
|
+
? (performanceObserverCtor as any).supportedEntryTypes
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
148
|
+
if (!supportedTypes || !supportedTypes.includes('measure') || !supportedTypes.includes('mark')) {
|
|
149
|
+
canObserveUserTiming = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
canObserveUserTiming = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (canObserveUserTiming) {
|
|
158
|
+
entryTypes.push('measure', 'mark');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (entryTypes.length === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 创建性能观察者
|
|
166
|
+
const observer = this._performanceManager.createObserver((entries) => {
|
|
167
|
+
this._handlePerformanceEntries(entries);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
observer.observe({ entryTypes });
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// 如果失败(例如微信小程序不支持 measure/mark),尝试移除这些类型后重试
|
|
174
|
+
const safeTypes = entryTypes.filter(t => t !== 'measure' && t !== 'mark');
|
|
175
|
+
|
|
176
|
+
if (safeTypes.length < entryTypes.length && safeTypes.length > 0) {
|
|
177
|
+
// 降级重试
|
|
178
|
+
observer.observe({ entryTypes: safeTypes });
|
|
179
|
+
console.warn('[Sentry Performance] Failed to observe all types, falling back to:', safeTypes);
|
|
180
|
+
|
|
181
|
+
// 更新 entryTypes 以便日志记录正确
|
|
182
|
+
entryTypes.length = 0;
|
|
183
|
+
entryTypes.push(...safeTypes);
|
|
184
|
+
} else {
|
|
185
|
+
throw e; // 如果没有可降级的类型或仍然失败,则抛出
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this._observers.push(observer);
|
|
190
|
+
|
|
191
|
+
const globalProcess = typeof globalThis !== 'undefined' ? (globalThis as any).process : undefined;
|
|
192
|
+
if (globalProcess && globalProcess.env && globalProcess.env.NODE_ENV !== 'production') {
|
|
193
|
+
console.log('[Sentry Performance] Performance observers setup for:', entryTypes);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.warn('[Sentry Performance] Failed to setup performance observers:', error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 处理性能条目
|
|
202
|
+
*/
|
|
203
|
+
private _handlePerformanceEntries(entries: PerformanceEntry[] | any): void {
|
|
204
|
+
if (!entries) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 确保 entries 是数组格式
|
|
209
|
+
let entriesArray: PerformanceEntry[];
|
|
210
|
+
if (Array.isArray(entries)) {
|
|
211
|
+
entriesArray = entries;
|
|
212
|
+
} else if (typeof entries === 'object' && entries.getEntries) {
|
|
213
|
+
// 微信小程序可能传入 PerformanceObserverEntryList 对象
|
|
214
|
+
entriesArray = entries.getEntries();
|
|
215
|
+
} else if (typeof entries === 'object') {
|
|
216
|
+
// 如果是单个对象,转换为数组
|
|
217
|
+
entriesArray = [entries];
|
|
218
|
+
} else {
|
|
219
|
+
console.warn('[Sentry Performance] Invalid entries format:', typeof entries);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (entriesArray.length === 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 采样控制
|
|
228
|
+
if (Math.random() > this._options.sampleRate) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
entriesArray.forEach(entry => {
|
|
233
|
+
try {
|
|
234
|
+
this._processPerformanceEntry(entry);
|
|
235
|
+
this._addToBuffer(entry);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.warn('[Sentry Performance] Failed to process performance entry:', error);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 处理单个性能条目
|
|
244
|
+
*/
|
|
245
|
+
private _processPerformanceEntry(entry: PerformanceEntry): void {
|
|
246
|
+
switch (entry.entryType) {
|
|
247
|
+
case 'navigation':
|
|
248
|
+
this._processNavigationEntry(entry as NavigationPerformanceEntry);
|
|
249
|
+
break;
|
|
250
|
+
case 'render':
|
|
251
|
+
this._processRenderEntry(entry as RenderPerformanceEntry);
|
|
252
|
+
break;
|
|
253
|
+
case 'resource':
|
|
254
|
+
this._processResourceEntry(entry as ResourcePerformanceEntry);
|
|
255
|
+
break;
|
|
256
|
+
case 'measure':
|
|
257
|
+
case 'mark':
|
|
258
|
+
this._processUserTimingEntry(entry as UserTimingPerformanceEntry);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
console.log('[Sentry Performance] Unknown entry type:', entry.entryType);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 处理导航性能条目
|
|
267
|
+
*/
|
|
268
|
+
private _processNavigationEntry(entry: NavigationPerformanceEntry): void {
|
|
269
|
+
// 添加面包屑
|
|
270
|
+
const scope = getCurrentScope();
|
|
271
|
+
scope.addBreadcrumb({
|
|
272
|
+
message: `页面导航: ${entry.name}`,
|
|
273
|
+
category: 'performance.navigation',
|
|
274
|
+
level: 'info',
|
|
275
|
+
data: {
|
|
276
|
+
duration: entry.duration,
|
|
277
|
+
appLaunchTime: entry.appLaunchTime,
|
|
278
|
+
pageReadyTime: entry.pageReadyTime,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
startSpan({
|
|
283
|
+
name: `Navigation: ${entry.name}`,
|
|
284
|
+
op: 'navigation',
|
|
285
|
+
startTime: entry.startTime / 1000, // 转换为秒
|
|
286
|
+
}, (span) => {
|
|
287
|
+
span.setAttributes({
|
|
288
|
+
'navigation.name': entry.name,
|
|
289
|
+
'navigation.duration': entry.duration,
|
|
290
|
+
'navigation.app_launch_time': entry.appLaunchTime || 0,
|
|
291
|
+
'navigation.page_ready_time': entry.pageReadyTime || 0,
|
|
292
|
+
'navigation.first_render_time': entry.firstRenderTime || 0,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
span.end((entry.startTime + entry.duration) / 1000);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 处理渲染性能条目
|
|
301
|
+
*/
|
|
302
|
+
private _processRenderEntry(entry: RenderPerformanceEntry): void {
|
|
303
|
+
startSpan({
|
|
304
|
+
name: `Render: ${entry.name}`,
|
|
305
|
+
op: 'render',
|
|
306
|
+
startTime: entry.startTime / 1000,
|
|
307
|
+
}, (span) => {
|
|
308
|
+
span.setAttributes({
|
|
309
|
+
'render.name': entry.name,
|
|
310
|
+
'render.duration': entry.duration,
|
|
311
|
+
'render.start': entry.renderStart || 0,
|
|
312
|
+
'render.end': entry.renderEnd || 0,
|
|
313
|
+
'render.script_start': entry.scriptStart || 0,
|
|
314
|
+
'render.script_end': entry.scriptEnd || 0,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
span.end((entry.startTime + entry.duration) / 1000);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 处理资源加载性能条目
|
|
323
|
+
*/
|
|
324
|
+
private _processResourceEntry(entry: ResourcePerformanceEntry): void {
|
|
325
|
+
startSpan({
|
|
326
|
+
name: `Resource: ${entry.name}`,
|
|
327
|
+
op: 'resource',
|
|
328
|
+
startTime: entry.startTime / 1000,
|
|
329
|
+
}, (span) => {
|
|
330
|
+
span.setAttributes({
|
|
331
|
+
'resource.name': entry.name,
|
|
332
|
+
'resource.duration': entry.duration,
|
|
333
|
+
'resource.type': entry.initiatorType || 'unknown',
|
|
334
|
+
'resource.transfer_size': entry.transferSize || 0,
|
|
335
|
+
'resource.encoded_size': entry.encodedBodySize || 0,
|
|
336
|
+
'resource.decoded_size': entry.decodedBodySize || 0,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// 网络时序信息
|
|
340
|
+
if (entry.fetchStart && entry.responseEnd) {
|
|
341
|
+
span.setAttributes({
|
|
342
|
+
'resource.fetch_start': entry.fetchStart,
|
|
343
|
+
'resource.response_end': entry.responseEnd,
|
|
344
|
+
'resource.network_time': entry.responseEnd - entry.fetchStart,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
span.end((entry.startTime + entry.duration) / 1000);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 处理用户自定义性能条目
|
|
354
|
+
*/
|
|
355
|
+
private _processUserTimingEntry(entry: UserTimingPerformanceEntry): void {
|
|
356
|
+
if (entry.entryType === 'measure') {
|
|
357
|
+
startSpan({
|
|
358
|
+
name: `Measure: ${entry.name}`,
|
|
359
|
+
op: 'measure',
|
|
360
|
+
startTime: entry.startTime / 1000,
|
|
361
|
+
}, (span) => {
|
|
362
|
+
span.setAttributes({
|
|
363
|
+
'measure.name': entry.name,
|
|
364
|
+
'measure.duration': entry.duration,
|
|
365
|
+
'measure.detail': entry.detail ? JSON.stringify(entry.detail) : undefined,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
span.end((entry.startTime + entry.duration) / 1000);
|
|
369
|
+
});
|
|
370
|
+
} else if (entry.entryType === 'mark') {
|
|
371
|
+
// 标记事件作为面包屑记录
|
|
372
|
+
const scope = getCurrentScope();
|
|
373
|
+
scope.addBreadcrumb({
|
|
374
|
+
message: `性能标记: ${entry.name}`,
|
|
375
|
+
category: 'performance.mark',
|
|
376
|
+
level: 'info',
|
|
377
|
+
data: {
|
|
378
|
+
timestamp: entry.startTime,
|
|
379
|
+
detail: entry.detail,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 添加到缓冲区
|
|
387
|
+
*/
|
|
388
|
+
private _addToBuffer(entry: PerformanceEntry): void {
|
|
389
|
+
this._entryBuffer.push(entry);
|
|
390
|
+
|
|
391
|
+
// 缓冲区溢出处理
|
|
392
|
+
if (this._entryBuffer.length > this._options.bufferSize) {
|
|
393
|
+
this._entryBuffer = this._entryBuffer.slice(-this._options.bufferSize);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 开始自动上报
|
|
399
|
+
*/
|
|
400
|
+
private _startAutoReporting(): void {
|
|
401
|
+
if (this._options.reportInterval <= 0) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this._reportTimer = setInterval(() => {
|
|
406
|
+
this._reportBufferedEntries();
|
|
407
|
+
}, this._options.reportInterval);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 上报缓冲的性能条目
|
|
412
|
+
*/
|
|
413
|
+
private _reportBufferedEntries(): void {
|
|
414
|
+
if (this._entryBuffer.length === 0) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const scope = getCurrentScope();
|
|
420
|
+
|
|
421
|
+
// 计算性能统计
|
|
422
|
+
const stats = this._calculatePerformanceStats();
|
|
423
|
+
|
|
424
|
+
scope.setContext('performance_summary', {
|
|
425
|
+
total_entries: this._entryBuffer.length,
|
|
426
|
+
navigation_count: this._entryBuffer.filter(e => e.entryType === 'navigation').length,
|
|
427
|
+
render_count: this._entryBuffer.filter(e => e.entryType === 'render').length,
|
|
428
|
+
resource_count: this._entryBuffer.filter(e => e.entryType === 'resource').length,
|
|
429
|
+
measure_count: this._entryBuffer.filter(e => e.entryType === 'measure').length,
|
|
430
|
+
mark_count: this._entryBuffer.filter(e => e.entryType === 'mark').length,
|
|
431
|
+
report_time: new Date().toISOString(),
|
|
432
|
+
...stats,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// 检查性能阈值并发送警告
|
|
436
|
+
this._checkPerformanceThresholds(stats);
|
|
437
|
+
|
|
438
|
+
// 使用小程序原生 API 上报性能数据
|
|
439
|
+
this._reportToNativeAPI();
|
|
440
|
+
|
|
441
|
+
// 清空缓冲区
|
|
442
|
+
this._entryBuffer = [];
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn('[Sentry Performance] Failed to report buffered entries:', error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 计算性能统计数据
|
|
450
|
+
*/
|
|
451
|
+
private _calculatePerformanceStats(): Record<string, any> {
|
|
452
|
+
const navigationEntries = this._entryBuffer.filter(e => e.entryType === 'navigation');
|
|
453
|
+
const renderEntries = this._entryBuffer.filter(e => e.entryType === 'render');
|
|
454
|
+
const resourceEntries = this._entryBuffer.filter(e => e.entryType === 'resource');
|
|
455
|
+
|
|
456
|
+
const stats: Record<string, any> = {};
|
|
457
|
+
|
|
458
|
+
// 导航性能统计
|
|
459
|
+
if (navigationEntries.length > 0) {
|
|
460
|
+
const durations = navigationEntries.map(e => e.duration);
|
|
461
|
+
stats['navigation_stats'] = {
|
|
462
|
+
avg_duration: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
463
|
+
max_duration: Math.max(...durations),
|
|
464
|
+
min_duration: Math.min(...durations),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 渲染性能统计
|
|
469
|
+
if (renderEntries.length > 0) {
|
|
470
|
+
const durations = renderEntries.map(e => e.duration);
|
|
471
|
+
stats['render_stats'] = {
|
|
472
|
+
avg_duration: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
473
|
+
max_duration: Math.max(...durations),
|
|
474
|
+
min_duration: Math.min(...durations),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 资源加载统计
|
|
479
|
+
if (resourceEntries.length > 0) {
|
|
480
|
+
const durations = resourceEntries.map(e => e.duration);
|
|
481
|
+
const sizes = resourceEntries
|
|
482
|
+
.map(e => (e as ResourcePerformanceEntry).transferSize || 0)
|
|
483
|
+
.filter(size => size > 0);
|
|
484
|
+
|
|
485
|
+
stats['resource_stats'] = {
|
|
486
|
+
avg_load_time: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
487
|
+
max_load_time: Math.max(...durations),
|
|
488
|
+
total_transfer_size: sizes.reduce((a, b) => a + b, 0),
|
|
489
|
+
avg_transfer_size: sizes.length > 0 ? sizes.reduce((a, b) => a + b, 0) / sizes.length : 0,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return stats;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 检查性能阈值
|
|
498
|
+
*/
|
|
499
|
+
private _checkPerformanceThresholds(stats: Record<string, any>): void {
|
|
500
|
+
const scope = getCurrentScope();
|
|
501
|
+
|
|
502
|
+
// 检查导航性能
|
|
503
|
+
if (stats['navigation_stats']?.avg_duration > 3000) {
|
|
504
|
+
scope.addBreadcrumb({
|
|
505
|
+
message: '页面导航性能较慢',
|
|
506
|
+
category: 'performance.warning',
|
|
507
|
+
level: 'warning',
|
|
508
|
+
data: {
|
|
509
|
+
avg_duration: stats['navigation_stats'].avg_duration,
|
|
510
|
+
threshold: 3000,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 检查渲染性能
|
|
516
|
+
if (stats['render_stats']?.avg_duration > 1000) {
|
|
517
|
+
scope.addBreadcrumb({
|
|
518
|
+
message: '页面渲染性能较慢',
|
|
519
|
+
category: 'performance.warning',
|
|
520
|
+
level: 'warning',
|
|
521
|
+
data: {
|
|
522
|
+
avg_duration: stats['render_stats'].avg_duration,
|
|
523
|
+
threshold: 1000,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 检查资源加载
|
|
529
|
+
if (stats['resource_stats']?.avg_load_time > 2000) {
|
|
530
|
+
scope.addBreadcrumb({
|
|
531
|
+
message: '资源加载性能较慢',
|
|
532
|
+
category: 'performance.warning',
|
|
533
|
+
level: 'warning',
|
|
534
|
+
data: {
|
|
535
|
+
avg_load_time: stats['resource_stats'].avg_load_time,
|
|
536
|
+
threshold: 2000,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* 使用小程序原生 API 上报性能数据
|
|
544
|
+
*/
|
|
545
|
+
private _reportToNativeAPI(): void {
|
|
546
|
+
try {
|
|
547
|
+
const currentSdk = sdk();
|
|
548
|
+
if (currentSdk.reportPerformance && this._entryBuffer.length > 0) {
|
|
549
|
+
const performanceData = {
|
|
550
|
+
entries: this._entryBuffer.map(entry => ({
|
|
551
|
+
name: entry.name,
|
|
552
|
+
entryType: entry.entryType,
|
|
553
|
+
startTime: entry.startTime,
|
|
554
|
+
duration: entry.duration,
|
|
555
|
+
})),
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
sampleRate: this._options.sampleRate,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
currentSdk.reportPerformance(performanceData);
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.warn('[Sentry Performance] Failed to report to native API:', error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 添加性能上下文信息
|
|
569
|
+
*/
|
|
570
|
+
private _addPerformanceContext(): void {
|
|
571
|
+
try {
|
|
572
|
+
const scope = getCurrentScope();
|
|
573
|
+
const currentSdk = sdk();
|
|
574
|
+
|
|
575
|
+
// 检查 Performance API 支持情况
|
|
576
|
+
const hasPerformanceAPI = !!(currentSdk.getPerformance);
|
|
577
|
+
const hasReportAPI = !!(currentSdk.reportPerformance);
|
|
578
|
+
|
|
579
|
+
scope.setContext('performance_support', {
|
|
580
|
+
has_performance_api: hasPerformanceAPI,
|
|
581
|
+
has_report_api: hasReportAPI,
|
|
582
|
+
integration_enabled: true,
|
|
583
|
+
options: this._options,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
scope.setTag('performance.integration', 'enabled');
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.warn('[Sentry Performance] Failed to add performance context:', error);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 清理资源
|
|
594
|
+
*/
|
|
595
|
+
public cleanup(): void {
|
|
596
|
+
// 断开所有观察者
|
|
597
|
+
this._observers.forEach(observer => {
|
|
598
|
+
try {
|
|
599
|
+
observer.disconnect();
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.warn('[Sentry Performance] Failed to disconnect observer:', error);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
this._observers = [];
|
|
605
|
+
|
|
606
|
+
// 清除定时器
|
|
607
|
+
if (this._reportTimer) {
|
|
608
|
+
clearInterval(this._reportTimer);
|
|
609
|
+
this._reportTimer = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 最后一次上报
|
|
613
|
+
this._reportBufferedEntries();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Performance API 集成工厂函数
|
|
619
|
+
*/
|
|
620
|
+
export const performanceIntegration = (options?: PerformanceIntegrationOptions): IntegrationFn => {
|
|
621
|
+
return () => new PerformanceIntegration(options);
|
|
622
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Event, Integration, Exception, StackFrame } from '@sentry/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize miniapp stack trace paths to a standard format for source map resolution.
|
|
5
|
+
* E.g., 'appservice/pages/index.js' -> 'app:///pages/index.js'
|
|
6
|
+
*/
|
|
7
|
+
export class RewriteFrames implements Integration {
|
|
8
|
+
/**
|
|
9
|
+
* @inheritDoc
|
|
10
|
+
*/
|
|
11
|
+
public static id: string = 'RewriteFrames';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @inheritDoc
|
|
15
|
+
*/
|
|
16
|
+
public name: string = RewriteFrames.id;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Optional prefix to append to the normalized path. Defaults to 'app:///'
|
|
20
|
+
*/
|
|
21
|
+
private readonly _prefix: string;
|
|
22
|
+
|
|
23
|
+
public constructor(options: { prefix?: string } = {}) {
|
|
24
|
+
this._prefix = options.prefix || 'app:///';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @inheritDoc
|
|
29
|
+
*/
|
|
30
|
+
public setupOnce(): void {
|
|
31
|
+
// In Sentry v10, we usually use addGlobalEventProcessor or similar to add an event processor
|
|
32
|
+
// For integrations, we can just return the processor in processEvent or register it globally.
|
|
33
|
+
// However, the cleanest way in v10 is to use `processEvent`.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @inheritDoc
|
|
38
|
+
*/
|
|
39
|
+
public processEvent(event: Event): Event {
|
|
40
|
+
if (event.exception && event.exception.values) {
|
|
41
|
+
event.exception.values.forEach((exception: Exception) => {
|
|
42
|
+
if (exception.stacktrace && exception.stacktrace.frames) {
|
|
43
|
+
exception.stacktrace.frames.forEach((frame: StackFrame) => {
|
|
44
|
+
if (frame.filename) {
|
|
45
|
+
frame.filename = this._normalizeFilename(frame.filename);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return event;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalizes a filename from various miniapp platforms
|
|
56
|
+
*/
|
|
57
|
+
private _normalizeFilename(filename: string): string {
|
|
58
|
+
let normalized = filename;
|
|
59
|
+
|
|
60
|
+
// Remove common platform prefixes
|
|
61
|
+
// WeChat: appservice/, app-service/, WAService.js
|
|
62
|
+
// Alipay: https://appx/...
|
|
63
|
+
// ByteDance: tt://...
|
|
64
|
+
normalized = normalized
|
|
65
|
+
.replace(/^(appservice|app-service|WAService)\//i, '')
|
|
66
|
+
.replace(/^https?:\/\/[^/]+\//i, '') // Remove alipay http(s) protocol and domain
|
|
67
|
+
.replace(/^[a-z]+:\/\//i, '') // Remove other protocols like tt://, swan://
|
|
68
|
+
.replace(/^\//, ''); // Remove leading slash if any
|
|
69
|
+
|
|
70
|
+
// Prevent double prefixing if it's already an absolute path
|
|
71
|
+
if (normalized.startsWith(this._prefix)) {
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `${this._prefix}${normalized}`;
|
|
76
|
+
}
|
|
77
|
+
}
|