sobey-monitor-sdk 1.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.
@@ -0,0 +1,1454 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * 默认配置
7
+ */
8
+ const DEFAULT_CONFIG = {
9
+ debug: false,
10
+ sampling: {
11
+ error: 1,
12
+ performance: 1,
13
+ behavior: 1,
14
+ },
15
+ report: {
16
+ maxBufferSize: 10,
17
+ flushInterval: 5000,
18
+ timeout: 10000,
19
+ },
20
+ error: {
21
+ enabled: true,
22
+ jsError: true,
23
+ promiseError: true,
24
+ resourceError: true,
25
+ httpError: true,
26
+ },
27
+ performance: {
28
+ enabled: true,
29
+ webVitals: true,
30
+ resource: true,
31
+ api: true,
32
+ },
33
+ behavior: {
34
+ enabled: true,
35
+ pv: true,
36
+ click: true,
37
+ route: true,
38
+ maxBreadcrumbs: 20,
39
+ },
40
+ };
41
+ /**
42
+ * 配置管理类
43
+ */
44
+ class ConfigManager {
45
+ constructor() {
46
+ this.config = null;
47
+ }
48
+ /**
49
+ * 初始化配置
50
+ */
51
+ init(userConfig) {
52
+ if (!userConfig.appId) {
53
+ throw new Error('[Monitor] appId is required');
54
+ }
55
+ if (!userConfig.dsn) {
56
+ throw new Error('[Monitor] dsn is required');
57
+ }
58
+ this.config = this.mergeConfig(DEFAULT_CONFIG, userConfig);
59
+ }
60
+ /**
61
+ * 获取配置
62
+ */
63
+ get() {
64
+ if (!this.config) {
65
+ throw new Error('[Monitor] SDK not initialized. Please call init() first.');
66
+ }
67
+ return this.config;
68
+ }
69
+ /**
70
+ * 更新配置
71
+ */
72
+ update(partialConfig) {
73
+ if (!this.config) {
74
+ throw new Error('[Monitor] SDK not initialized. Please call init() first.');
75
+ }
76
+ this.config = this.mergeConfig(this.config, partialConfig);
77
+ }
78
+ /**
79
+ * 设置用户信息
80
+ */
81
+ setUser(user) {
82
+ if (this.config) {
83
+ this.config.user = { ...this.config.user, ...user };
84
+ }
85
+ }
86
+ /**
87
+ * 是否已初始化
88
+ */
89
+ isInitialized() {
90
+ return this.config !== null;
91
+ }
92
+ /**
93
+ * 合并配置
94
+ */
95
+ mergeConfig(defaultConfig, userConfig) {
96
+ const result = { ...defaultConfig };
97
+ for (const key in userConfig) {
98
+ if (Object.prototype.hasOwnProperty.call(userConfig, key)) {
99
+ const userValue = userConfig[key];
100
+ const defaultValue = defaultConfig[key];
101
+ if (userValue !== null &&
102
+ typeof userValue === 'object' &&
103
+ !Array.isArray(userValue) &&
104
+ defaultValue !== null &&
105
+ typeof defaultValue === 'object' &&
106
+ !Array.isArray(defaultValue)) {
107
+ result[key] = this.mergeConfig(defaultValue, userValue);
108
+ }
109
+ else {
110
+ result[key] = userValue;
111
+ }
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ }
117
+ const config = new ConfigManager();
118
+
119
+ /**
120
+ * 上下文管理类
121
+ */
122
+ class ContextManager {
123
+ constructor() {
124
+ this.sessionId = '';
125
+ this.deviceInfo = null;
126
+ this.breadcrumbs = [];
127
+ this.maxBreadcrumbs = 20;
128
+ }
129
+ /**
130
+ * 初始化上下文
131
+ */
132
+ init(maxBreadcrumbs = 20) {
133
+ this.sessionId = this.generateSessionId();
134
+ this.deviceInfo = this.collectDeviceInfo();
135
+ this.maxBreadcrumbs = maxBreadcrumbs;
136
+ this.breadcrumbs = [];
137
+ }
138
+ /**
139
+ * 获取会话 ID
140
+ */
141
+ getSessionId() {
142
+ return this.sessionId;
143
+ }
144
+ /**
145
+ * 获取设备信息
146
+ */
147
+ getDeviceInfo() {
148
+ return this.deviceInfo;
149
+ }
150
+ /**
151
+ * 添加面包屑
152
+ */
153
+ addBreadcrumb(crumb) {
154
+ const breadcrumb = {
155
+ ...crumb,
156
+ timestamp: Date.now(),
157
+ };
158
+ this.breadcrumbs.push(breadcrumb);
159
+ // 限制数量
160
+ if (this.breadcrumbs.length > this.maxBreadcrumbs) {
161
+ this.breadcrumbs.shift();
162
+ }
163
+ }
164
+ /**
165
+ * 获取所有面包屑
166
+ */
167
+ getBreadcrumbs() {
168
+ return [...this.breadcrumbs];
169
+ }
170
+ /**
171
+ * 清空面包屑
172
+ */
173
+ clearBreadcrumbs() {
174
+ this.breadcrumbs = [];
175
+ }
176
+ /**
177
+ * 生成会话 ID
178
+ */
179
+ generateSessionId() {
180
+ const timestamp = Date.now().toString(36);
181
+ const randomPart = Math.random().toString(36).substring(2, 10);
182
+ return `${timestamp}-${randomPart}`;
183
+ }
184
+ /**
185
+ * 收集设备信息
186
+ */
187
+ collectDeviceInfo() {
188
+ return {
189
+ screenWidth: window.screen.width,
190
+ screenHeight: window.screen.height,
191
+ viewportWidth: window.innerWidth,
192
+ viewportHeight: window.innerHeight,
193
+ devicePixelRatio: window.devicePixelRatio || 1,
194
+ language: navigator.language,
195
+ platform: navigator.platform,
196
+ };
197
+ }
198
+ }
199
+ const context = new ContextManager();
200
+
201
+ /**
202
+ * 浏览器信息工具
203
+ */
204
+ /**
205
+ * 获取浏览器 User-Agent
206
+ */
207
+ function getUserAgent() {
208
+ return navigator.userAgent;
209
+ }
210
+ /**
211
+ * 获取当前页面 URL
212
+ */
213
+ function getPageUrl() {
214
+ return window.location.href;
215
+ }
216
+ /**
217
+ * 获取页面来源
218
+ */
219
+ function getReferrer() {
220
+ return document.referrer;
221
+ }
222
+ /**
223
+ * 获取页面标题
224
+ */
225
+ function getPageTitle() {
226
+ return document.title;
227
+ }
228
+ /**
229
+ * 判断是否支持 sendBeacon
230
+ */
231
+ function supportsSendBeacon() {
232
+ return typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function';
233
+ }
234
+ /**
235
+ * 判断页面是否即将卸载
236
+ */
237
+ function isPageHidden() {
238
+ return document.visibilityState === 'hidden';
239
+ }
240
+
241
+ /**
242
+ * 数据发送器
243
+ */
244
+ /**
245
+ * 发送器类
246
+ */
247
+ class Sender {
248
+ constructor() {
249
+ this.buffer = [];
250
+ this.timer = null;
251
+ }
252
+ /**
253
+ * 发送单条数据
254
+ */
255
+ send(data) {
256
+ const cfg = config.get();
257
+ // 添加到缓冲区
258
+ this.buffer.push(data);
259
+ // 达到缓冲上限,立即发送
260
+ if (this.buffer.length >= (cfg.report?.maxBufferSize || 10)) {
261
+ this.flush();
262
+ return;
263
+ }
264
+ // 启动定时器
265
+ this.scheduleFlush();
266
+ }
267
+ /**
268
+ * 立即发送所有缓冲数据
269
+ */
270
+ flush() {
271
+ if (this.buffer.length === 0)
272
+ return;
273
+ const data = [...this.buffer];
274
+ this.buffer = [];
275
+ this.clearTimer();
276
+ this.doSend(data);
277
+ }
278
+ /**
279
+ * 执行发送
280
+ */
281
+ doSend(data) {
282
+ const cfg = config.get();
283
+ const dsn = cfg.dsn;
284
+ const payload = JSON.stringify(data);
285
+ // 页面卸载时优先使用 sendBeacon
286
+ if (isPageHidden() && supportsSendBeacon()) {
287
+ this.sendByBeacon(dsn, payload);
288
+ return;
289
+ }
290
+ // 正常情况使用 fetch
291
+ this.sendByFetch(dsn, payload);
292
+ }
293
+ /**
294
+ * 使用 sendBeacon 发送
295
+ */
296
+ sendByBeacon(url, payload) {
297
+ try {
298
+ return navigator.sendBeacon(url, payload);
299
+ }
300
+ catch (e) {
301
+ if (config.get().debug) {
302
+ console.error('[Monitor] sendBeacon failed:', e);
303
+ }
304
+ // 降级到 fetch
305
+ this.sendByFetch(url, payload);
306
+ return false;
307
+ }
308
+ }
309
+ /**
310
+ * 使用 fetch 发送
311
+ */
312
+ sendByFetch(url, payload) {
313
+ const cfg = config.get();
314
+ fetch(url, {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Content-Type': 'application/json',
318
+ },
319
+ body: payload,
320
+ keepalive: true,
321
+ }).catch((e) => {
322
+ if (cfg.debug) {
323
+ console.error('[Monitor] fetch failed:', e);
324
+ }
325
+ // 发送失败,将数据放回缓冲区(可选)
326
+ // this.buffer.unshift(...JSON.parse(payload));
327
+ });
328
+ }
329
+ /**
330
+ * 调度延迟发送
331
+ */
332
+ scheduleFlush() {
333
+ if (this.timer)
334
+ return;
335
+ const cfg = config.get();
336
+ const interval = cfg.report?.flushInterval || 5000;
337
+ this.timer = setTimeout(() => {
338
+ this.flush();
339
+ }, interval);
340
+ }
341
+ /**
342
+ * 清除定时器
343
+ */
344
+ clearTimer() {
345
+ if (this.timer) {
346
+ clearTimeout(this.timer);
347
+ this.timer = null;
348
+ }
349
+ }
350
+ }
351
+ const sender = new Sender();
352
+ // 页面卸载时发送剩余数据
353
+ if (typeof window !== 'undefined') {
354
+ window.addEventListener('beforeunload', () => {
355
+ sender.flush();
356
+ });
357
+ window.addEventListener('visibilitychange', () => {
358
+ if (document.visibilityState === 'hidden') {
359
+ sender.flush();
360
+ }
361
+ });
362
+ }
363
+
364
+ /**
365
+ * 数据上报管理
366
+ */
367
+ // SDK 版本
368
+ const SDK_VERSION$1 = '1.0.0';
369
+ /**
370
+ * 上报器类
371
+ */
372
+ class Reporter {
373
+ /**
374
+ * 构建基础数据
375
+ */
376
+ buildBaseData() {
377
+ const cfg = config.get();
378
+ return {
379
+ appId: cfg.appId,
380
+ userId: cfg.user?.userId,
381
+ sessionId: context.getSessionId(),
382
+ pageUrl: getPageUrl(),
383
+ timestamp: Date.now(),
384
+ userAgent: getUserAgent(),
385
+ sdkVersion: cfg.version || SDK_VERSION$1,
386
+ };
387
+ }
388
+ /**
389
+ * 采样判断
390
+ */
391
+ shouldSample(type) {
392
+ const cfg = config.get();
393
+ const rate = cfg.sampling?.[type] ?? 1;
394
+ return Math.random() < rate;
395
+ }
396
+ /**
397
+ * 上报错误
398
+ */
399
+ reportError(data) {
400
+ if (!this.shouldSample('error'))
401
+ return;
402
+ const cfg = config.get();
403
+ // 过滤
404
+ if (cfg.error?.filter) {
405
+ const fullData = { ...this.buildBaseData(), ...data };
406
+ if (!cfg.error.filter(fullData))
407
+ return;
408
+ }
409
+ const reportData = {
410
+ ...this.buildBaseData(),
411
+ ...data,
412
+ breadcrumbs: context.getBreadcrumbs(),
413
+ deviceInfo: context.getDeviceInfo() || undefined,
414
+ };
415
+ this.send(reportData);
416
+ }
417
+ /**
418
+ * 上报性能
419
+ */
420
+ reportPerformance(data) {
421
+ if (!this.shouldSample('performance'))
422
+ return;
423
+ const reportData = {
424
+ ...this.buildBaseData(),
425
+ ...data,
426
+ };
427
+ this.send(reportData);
428
+ }
429
+ /**
430
+ * 上报行为
431
+ */
432
+ reportBehavior(data) {
433
+ if (!this.shouldSample('behavior'))
434
+ return;
435
+ const reportData = {
436
+ ...this.buildBaseData(),
437
+ ...data,
438
+ };
439
+ this.send(reportData);
440
+ }
441
+ /**
442
+ * 通用发送
443
+ */
444
+ send(data) {
445
+ const cfg = config.get();
446
+ if (cfg.debug) {
447
+ console.log('[Monitor] Report:', data);
448
+ }
449
+ sender.send(data);
450
+ }
451
+ /**
452
+ * 立即发送缓冲区数据
453
+ */
454
+ flush() {
455
+ sender.flush();
456
+ }
457
+ }
458
+ const reporter = new Reporter();
459
+
460
+ /**
461
+ * JS 运行时错误捕获插件
462
+ */
463
+ /**
464
+ * 安装 JS 错误监听
465
+ */
466
+ function installJsErrorHandler() {
467
+ const cfg = config.get();
468
+ if (!cfg.error?.enabled || !cfg.error?.jsError) {
469
+ return;
470
+ }
471
+ window.addEventListener('error', (event) => {
472
+ // 过滤资源加载错误(由 resourceError 处理)
473
+ if (event.target !== window) {
474
+ return;
475
+ }
476
+ const errorData = {
477
+ type: 'js_error',
478
+ message: event.message || 'Unknown error',
479
+ stack: event.error?.stack,
480
+ filename: event.filename,
481
+ lineno: event.lineno,
482
+ colno: event.colno,
483
+ };
484
+ // 添加到面包屑
485
+ context.addBreadcrumb({
486
+ type: 'console',
487
+ category: 'error',
488
+ data: {
489
+ message: errorData.message,
490
+ filename: errorData.filename,
491
+ lineno: errorData.lineno,
492
+ },
493
+ });
494
+ reporter.reportError(errorData);
495
+ }, true);
496
+ if (cfg.debug) {
497
+ console.log('[Monitor] JS error handler installed');
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Promise 未捕获错误插件
503
+ */
504
+ /**
505
+ * 安装 Promise 错误监听
506
+ */
507
+ function installPromiseErrorHandler() {
508
+ const cfg = config.get();
509
+ if (!cfg.error?.enabled || !cfg.error?.promiseError) {
510
+ return;
511
+ }
512
+ window.addEventListener('unhandledrejection', (event) => {
513
+ let message = 'Unhandled Promise rejection';
514
+ let stack;
515
+ const reason = event.reason;
516
+ if (reason instanceof Error) {
517
+ message = reason.message;
518
+ stack = reason.stack;
519
+ }
520
+ else if (typeof reason === 'string') {
521
+ message = reason;
522
+ }
523
+ else if (reason && typeof reason === 'object') {
524
+ message = JSON.stringify(reason);
525
+ }
526
+ const errorData = {
527
+ type: 'promise_error',
528
+ message,
529
+ stack,
530
+ };
531
+ // 添加到面包屑
532
+ context.addBreadcrumb({
533
+ type: 'console',
534
+ category: 'promise_error',
535
+ data: {
536
+ message: errorData.message,
537
+ },
538
+ });
539
+ reporter.reportError(errorData);
540
+ });
541
+ if (cfg.debug) {
542
+ console.log('[Monitor] Promise error handler installed');
543
+ }
544
+ }
545
+
546
+ /**
547
+ * 资源加载错误捕获插件
548
+ */
549
+ /**
550
+ * 安装资源错误监听
551
+ */
552
+ function installResourceErrorHandler() {
553
+ const cfg = config.get();
554
+ if (!cfg.error?.enabled || !cfg.error?.resourceError) {
555
+ return;
556
+ }
557
+ window.addEventListener('error', (event) => {
558
+ const target = event.target;
559
+ // 只处理资源加载错误(target 不是 window)
560
+ if (!target || target === window || !(target instanceof HTMLElement)) {
561
+ return;
562
+ }
563
+ const tagName = target.tagName.toLowerCase();
564
+ // 只监控特定标签的资源
565
+ if (!['img', 'script', 'link', 'video', 'audio', 'source'].includes(tagName)) {
566
+ return;
567
+ }
568
+ // 获取资源 URL
569
+ const resourceUrl = target.src
570
+ || target.href
571
+ || '';
572
+ if (!resourceUrl) {
573
+ return;
574
+ }
575
+ const errorData = {
576
+ type: 'resource_error',
577
+ message: `Failed to load ${tagName}: ${resourceUrl}`,
578
+ resourceUrl,
579
+ };
580
+ // 添加到面包屑
581
+ context.addBreadcrumb({
582
+ type: 'request',
583
+ category: 'resource_error',
584
+ data: {
585
+ tagName,
586
+ resourceUrl,
587
+ },
588
+ });
589
+ reporter.reportError(errorData);
590
+ }, true); // 使用捕获阶段
591
+ if (cfg.debug) {
592
+ console.log('[Monitor] Resource error handler installed');
593
+ }
594
+ }
595
+
596
+ /**
597
+ * HTTP 请求错误拦截插件
598
+ * 拦截 XMLHttpRequest 和 Fetch 请求
599
+ */
600
+ // 保存原始方法
601
+ const originalXHROpen = XMLHttpRequest.prototype.open;
602
+ const originalXHRSend = XMLHttpRequest.prototype.send;
603
+ const originalFetch = window.fetch;
604
+ /**
605
+ * 安装 HTTP 错误拦截
606
+ */
607
+ function installHttpErrorHandler() {
608
+ const cfg = config.get();
609
+ if (!cfg.error?.enabled || !cfg.error?.httpError) {
610
+ return;
611
+ }
612
+ interceptXHR();
613
+ interceptFetch();
614
+ if (cfg.debug) {
615
+ console.log('[Monitor] HTTP error handler installed');
616
+ }
617
+ }
618
+ /**
619
+ * 拦截 XMLHttpRequest
620
+ */
621
+ function interceptXHR() {
622
+ XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
623
+ // 存储请求信息
624
+ this._monitorData = {
625
+ method,
626
+ url: url.toString(),
627
+ startTime: 0,
628
+ };
629
+ return originalXHROpen.call(this, method, url, async, username, password);
630
+ };
631
+ XMLHttpRequest.prototype.send = function (body) {
632
+ const monitorData = this._monitorData;
633
+ if (monitorData) {
634
+ monitorData.startTime = Date.now();
635
+ monitorData.requestBody = typeof body === 'string' ? body : undefined;
636
+ }
637
+ this.addEventListener('loadend', function () {
638
+ if (!monitorData)
639
+ return;
640
+ const duration = Date.now() - monitorData.startTime;
641
+ const status = this.status;
642
+ // 添加到面包屑
643
+ context.addBreadcrumb({
644
+ type: 'request',
645
+ category: 'xhr',
646
+ data: {
647
+ method: monitorData.method,
648
+ url: monitorData.url,
649
+ status,
650
+ duration,
651
+ },
652
+ });
653
+ // 只上报错误请求 (4xx, 5xx 或 0)
654
+ if (status === 0 || status >= 400) {
655
+ reportHttpError({
656
+ method: monitorData.method,
657
+ url: monitorData.url,
658
+ status,
659
+ duration,
660
+ requestBody: monitorData.requestBody,
661
+ responseBody: this.responseText?.substring(0, 1000), // 限制长度
662
+ });
663
+ }
664
+ });
665
+ return originalXHRSend.call(this, body);
666
+ };
667
+ }
668
+ /**
669
+ * 拦截 Fetch
670
+ */
671
+ function interceptFetch() {
672
+ window.fetch = async function (input, init) {
673
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
674
+ const method = init?.method || 'GET';
675
+ const startTime = Date.now();
676
+ const requestBody = typeof init?.body === 'string' ? init.body : undefined;
677
+ try {
678
+ const response = await originalFetch.call(window, input, init);
679
+ const duration = Date.now() - startTime;
680
+ const status = response.status;
681
+ // 添加到面包屑
682
+ context.addBreadcrumb({
683
+ type: 'request',
684
+ category: 'fetch',
685
+ data: {
686
+ method,
687
+ url,
688
+ status,
689
+ duration,
690
+ },
691
+ });
692
+ // 只上报错误请求
693
+ if (!response.ok) {
694
+ // 克隆响应以读取 body
695
+ const cloned = response.clone();
696
+ let responseBody;
697
+ try {
698
+ responseBody = await cloned.text();
699
+ responseBody = responseBody.substring(0, 1000);
700
+ }
701
+ catch { }
702
+ reportHttpError({
703
+ method,
704
+ url,
705
+ status,
706
+ duration,
707
+ requestBody,
708
+ responseBody,
709
+ });
710
+ }
711
+ return response;
712
+ }
713
+ catch (error) {
714
+ const duration = Date.now() - startTime;
715
+ // 网络错误
716
+ context.addBreadcrumb({
717
+ type: 'request',
718
+ category: 'fetch',
719
+ data: {
720
+ method,
721
+ url,
722
+ status: 0,
723
+ duration,
724
+ error: error.message,
725
+ },
726
+ });
727
+ reportHttpError({
728
+ method,
729
+ url,
730
+ status: 0,
731
+ duration,
732
+ requestBody,
733
+ });
734
+ throw error;
735
+ }
736
+ };
737
+ }
738
+ /**
739
+ * 上报 HTTP 错误
740
+ */
741
+ function reportHttpError(httpInfo) {
742
+ const errorData = {
743
+ type: 'http_error',
744
+ message: `HTTP ${httpInfo.status} ${httpInfo.method} ${httpInfo.url}`,
745
+ httpInfo,
746
+ };
747
+ reporter.reportError(errorData);
748
+ }
749
+
750
+ /**
751
+ * 白屏检测插件
752
+ * 使用 DOM 采样检测页面是否白屏
753
+ */
754
+ // 采样点 - 页面关键区域
755
+ const SAMPLE_POINTS = [
756
+ { x: 0.5, y: 0.1 }, // 顶部中间
757
+ { x: 0.25, y: 0.5 }, // 左中
758
+ { x: 0.5, y: 0.5 }, // 中心
759
+ { x: 0.75, y: 0.5 }, // 右中
760
+ { x: 0.5, y: 0.9 }, // 底部中间
761
+ ];
762
+ // 无效元素
763
+ const INVALID_ELEMENTS = ['html', 'body', 'head', 'meta', 'link', 'style', 'script'];
764
+ /**
765
+ * 安装白屏检测
766
+ */
767
+ function installWhiteScreenDetector() {
768
+ const cfg = config.get();
769
+ if (!cfg.error?.enabled) {
770
+ return;
771
+ }
772
+ // 在页面加载完成后检测
773
+ if (document.readyState === 'complete') {
774
+ scheduleDetection();
775
+ }
776
+ else {
777
+ window.addEventListener('load', () => {
778
+ scheduleDetection();
779
+ });
780
+ }
781
+ if (cfg.debug) {
782
+ console.log('[Monitor] White screen detector installed');
783
+ }
784
+ }
785
+ /**
786
+ * 调度检测(延迟执行,给页面渲染时间)
787
+ */
788
+ function scheduleDetection() {
789
+ // 延迟 1 秒检测
790
+ setTimeout(() => {
791
+ const isWhiteScreen = detectWhiteScreen();
792
+ if (isWhiteScreen) {
793
+ reportWhiteScreen();
794
+ }
795
+ }, 1000);
796
+ }
797
+ /**
798
+ * 检测是否白屏
799
+ */
800
+ function detectWhiteScreen() {
801
+ const viewportWidth = window.innerWidth;
802
+ const viewportHeight = window.innerHeight;
803
+ let emptyPoints = 0;
804
+ for (const point of SAMPLE_POINTS) {
805
+ const x = viewportWidth * point.x;
806
+ const y = viewportHeight * point.y;
807
+ const element = document.elementFromPoint(x, y);
808
+ if (!element || isInvalidElement(element)) {
809
+ emptyPoints++;
810
+ }
811
+ }
812
+ // 如果超过 80% 的采样点是空的,认为是白屏
813
+ return emptyPoints / SAMPLE_POINTS.length > 0.8;
814
+ }
815
+ /**
816
+ * 判断是否为无效元素
817
+ */
818
+ function isInvalidElement(element) {
819
+ const tagName = element.tagName.toLowerCase();
820
+ return INVALID_ELEMENTS.includes(tagName);
821
+ }
822
+ /**
823
+ * 上报白屏错误
824
+ */
825
+ function reportWhiteScreen() {
826
+ const errorData = {
827
+ type: 'white_screen',
828
+ message: 'White screen detected',
829
+ };
830
+ reporter.reportError(errorData);
831
+ }
832
+
833
+ /**
834
+ * 错误监控插件入口
835
+ */
836
+ /**
837
+ * 安装所有错误监控
838
+ */
839
+ function installErrorHandlers() {
840
+ installJsErrorHandler();
841
+ installPromiseErrorHandler();
842
+ installResourceErrorHandler();
843
+ installHttpErrorHandler();
844
+ installWhiteScreenDetector();
845
+ }
846
+
847
+ /**
848
+ * Web Vitals 性能指标采集插件
849
+ * 采集 FP, FCP, LCP, FID, CLS, TTFB 等核心指标
850
+ */
851
+ /**
852
+ * 安装 Web Vitals 采集
853
+ */
854
+ function installWebVitals() {
855
+ const cfg = config.get();
856
+ if (!cfg.performance?.enabled || !cfg.performance?.webVitals) {
857
+ return;
858
+ }
859
+ // 页面加载完成后采集
860
+ if (document.readyState === 'complete') {
861
+ collectMetrics();
862
+ }
863
+ else {
864
+ window.addEventListener('load', () => {
865
+ // 延迟采集,确保 LCP 等指标稳定
866
+ setTimeout(collectMetrics, 3000);
867
+ });
868
+ }
869
+ if (cfg.debug) {
870
+ console.log('[Monitor] Web Vitals collector installed');
871
+ }
872
+ }
873
+ /**
874
+ * 采集性能指标
875
+ */
876
+ function collectMetrics() {
877
+ const metrics = {};
878
+ // 使用 Performance API
879
+ if (!window.performance) {
880
+ return;
881
+ }
882
+ // 获取导航计时
883
+ const navigation = performance.getEntriesByType('navigation')[0];
884
+ if (navigation) {
885
+ metrics.ttfb = navigation.responseStart - navigation.requestStart;
886
+ metrics.domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
887
+ metrics.loadComplete = navigation.loadEventEnd - navigation.startTime;
888
+ }
889
+ // 获取 FP 和 FCP
890
+ const paintEntries = performance.getEntriesByType('paint');
891
+ for (const entry of paintEntries) {
892
+ if (entry.name === 'first-paint') {
893
+ metrics.fp = entry.startTime;
894
+ }
895
+ if (entry.name === 'first-contentful-paint') {
896
+ metrics.fcp = entry.startTime;
897
+ }
898
+ }
899
+ // 获取 LCP
900
+ collectLCP(metrics);
901
+ // 获取 FID
902
+ collectFID(metrics);
903
+ // 获取 CLS
904
+ collectCLS(metrics);
905
+ // 上报性能数据
906
+ reporter.reportPerformance({
907
+ type: 'performance',
908
+ metrics,
909
+ });
910
+ }
911
+ /**
912
+ * 采集 LCP (Largest Contentful Paint)
913
+ */
914
+ function collectLCP(metrics) {
915
+ if (!('PerformanceObserver' in window))
916
+ return;
917
+ try {
918
+ const observer = new PerformanceObserver((list) => {
919
+ const entries = list.getEntries();
920
+ const lastEntry = entries[entries.length - 1];
921
+ if (lastEntry) {
922
+ metrics.lcp = lastEntry.startTime;
923
+ }
924
+ });
925
+ observer.observe({ type: 'largest-contentful-paint', buffered: true });
926
+ }
927
+ catch (e) {
928
+ // 浏览器不支持
929
+ }
930
+ }
931
+ /**
932
+ * 采集 FID (First Input Delay)
933
+ */
934
+ function collectFID(metrics) {
935
+ if (!('PerformanceObserver' in window))
936
+ return;
937
+ try {
938
+ const observer = new PerformanceObserver((list) => {
939
+ const entries = list.getEntries();
940
+ const firstEntry = entries[0];
941
+ if (firstEntry) {
942
+ metrics.fid = firstEntry.processingStart - firstEntry.startTime;
943
+ }
944
+ });
945
+ observer.observe({ type: 'first-input', buffered: true });
946
+ }
947
+ catch (e) {
948
+ // 浏览器不支持
949
+ }
950
+ }
951
+ /**
952
+ * 采集 CLS (Cumulative Layout Shift)
953
+ */
954
+ function collectCLS(metrics) {
955
+ if (!('PerformanceObserver' in window))
956
+ return;
957
+ let clsValue = 0;
958
+ try {
959
+ const observer = new PerformanceObserver((list) => {
960
+ const entries = list.getEntries();
961
+ for (const entry of entries) {
962
+ if (!entry.hadRecentInput) {
963
+ clsValue += entry.value;
964
+ }
965
+ }
966
+ metrics.cls = clsValue;
967
+ });
968
+ observer.observe({ type: 'layout-shift', buffered: true });
969
+ }
970
+ catch (e) {
971
+ // 浏览器不支持
972
+ }
973
+ }
974
+
975
+ /**
976
+ * 性能监控插件入口
977
+ */
978
+ /**
979
+ * 安装性能监控
980
+ */
981
+ function installPerformanceMonitor() {
982
+ installWebVitals();
983
+ }
984
+
985
+ /**
986
+ * PV (Page View) 统计插件
987
+ */
988
+ /**
989
+ * 安装 PV 统计
990
+ */
991
+ function installPVTracker() {
992
+ const cfg = config.get();
993
+ if (!cfg.behavior?.enabled || !cfg.behavior?.pv) {
994
+ return;
995
+ }
996
+ // 页面加载时上报
997
+ reportPV();
998
+ if (cfg.debug) {
999
+ console.log('[Monitor] PV tracker installed');
1000
+ }
1001
+ }
1002
+ /**
1003
+ * 上报 PV
1004
+ */
1005
+ function reportPV() {
1006
+ reporter.reportBehavior({
1007
+ type: 'behavior',
1008
+ action: 'pv',
1009
+ data: {
1010
+ title: getPageTitle(),
1011
+ referrer: getReferrer(),
1012
+ },
1013
+ });
1014
+ }
1015
+
1016
+ /**
1017
+ * 点击事件追踪插件
1018
+ */
1019
+ /**
1020
+ * 安装点击追踪
1021
+ */
1022
+ function installClickTracker() {
1023
+ const cfg = config.get();
1024
+ if (!cfg.behavior?.enabled || !cfg.behavior?.click) {
1025
+ return;
1026
+ }
1027
+ document.addEventListener('click', (event) => {
1028
+ const target = event.target;
1029
+ if (!target)
1030
+ return;
1031
+ const clickData = extractClickData(target, event);
1032
+ // 添加到面包屑
1033
+ context.addBreadcrumb({
1034
+ type: 'click',
1035
+ category: 'ui.click',
1036
+ data: clickData,
1037
+ });
1038
+ // 可选:上报点击事件(一般只记录面包屑,不单独上报)
1039
+ // reporter.reportBehavior({
1040
+ // type: 'behavior',
1041
+ // action: 'click',
1042
+ // data: clickData,
1043
+ // });
1044
+ }, true);
1045
+ if (cfg.debug) {
1046
+ console.log('[Monitor] Click tracker installed');
1047
+ }
1048
+ }
1049
+ /**
1050
+ * 提取点击数据
1051
+ */
1052
+ function extractClickData(element, event) {
1053
+ const tagName = element.tagName.toLowerCase();
1054
+ const rect = element.getBoundingClientRect();
1055
+ return {
1056
+ tagName,
1057
+ id: element.id || undefined,
1058
+ className: element.className || undefined,
1059
+ text: getElementText(element),
1060
+ path: getElementPath(element),
1061
+ // 鼠标坐标
1062
+ x: event.clientX,
1063
+ y: event.clientY,
1064
+ // 元素位置和尺寸
1065
+ rect: {
1066
+ top: Math.round(rect.top),
1067
+ left: Math.round(rect.left),
1068
+ width: Math.round(rect.width),
1069
+ height: Math.round(rect.height),
1070
+ },
1071
+ // 额外属性
1072
+ href: element.href || undefined,
1073
+ name: element.name || undefined,
1074
+ type: element.type || undefined,
1075
+ };
1076
+ }
1077
+ /**
1078
+ * 获取元素文本(限制长度)
1079
+ */
1080
+ function getElementText(element) {
1081
+ const text = element.innerText || element.textContent || '';
1082
+ return text.trim().substring(0, 50);
1083
+ }
1084
+ /**
1085
+ * 获取元素路径
1086
+ */
1087
+ function getElementPath(element) {
1088
+ const path = [];
1089
+ let current = element;
1090
+ while (current && path.length < 5) {
1091
+ let selector = current.tagName.toLowerCase();
1092
+ if (current.id) {
1093
+ selector += `#${current.id}`;
1094
+ }
1095
+ else if (current.className) {
1096
+ const classes = current.className.split(' ').filter(Boolean).slice(0, 2);
1097
+ if (classes.length) {
1098
+ selector += `.${classes.join('.')}`;
1099
+ }
1100
+ }
1101
+ path.unshift(selector);
1102
+ current = current.parentElement;
1103
+ }
1104
+ return path.join(' > ');
1105
+ }
1106
+
1107
+ /**
1108
+ * 路由变化监控插件
1109
+ * 支持 History API 和 Hash 路由
1110
+ */
1111
+ // 保存原始方法
1112
+ const originalPushState = history.pushState;
1113
+ const originalReplaceState = history.replaceState;
1114
+ /**
1115
+ * 安装路由监控
1116
+ */
1117
+ function installRouteTracker() {
1118
+ const cfg = config.get();
1119
+ if (!cfg.behavior?.enabled || !cfg.behavior?.route) {
1120
+ return;
1121
+ }
1122
+ // 拦截 pushState
1123
+ history.pushState = function (...args) {
1124
+ const result = originalPushState.apply(this, args);
1125
+ handleRouteChange('pushState');
1126
+ return result;
1127
+ };
1128
+ // 拦截 replaceState
1129
+ history.replaceState = function (...args) {
1130
+ const result = originalReplaceState.apply(this, args);
1131
+ handleRouteChange('replaceState');
1132
+ return result;
1133
+ };
1134
+ // 监听 popstate(浏览器前进后退)
1135
+ window.addEventListener('popstate', () => {
1136
+ handleRouteChange('popstate');
1137
+ });
1138
+ // 监听 hashchange
1139
+ window.addEventListener('hashchange', () => {
1140
+ handleRouteChange('hashchange');
1141
+ });
1142
+ if (cfg.debug) {
1143
+ console.log('[Monitor] Route tracker installed');
1144
+ }
1145
+ }
1146
+ /**
1147
+ * 处理路由变化
1148
+ */
1149
+ function handleRouteChange(trigger) {
1150
+ const routeData = {
1151
+ url: getPageUrl(),
1152
+ title: getPageTitle(),
1153
+ trigger,
1154
+ };
1155
+ // 添加到面包屑
1156
+ context.addBreadcrumb({
1157
+ type: 'route',
1158
+ category: 'navigation',
1159
+ data: routeData,
1160
+ });
1161
+ // 上报路由变化
1162
+ reporter.reportBehavior({
1163
+ type: 'behavior',
1164
+ action: 'route',
1165
+ data: routeData,
1166
+ });
1167
+ }
1168
+
1169
+ /**
1170
+ * 控制台日志追踪插件
1171
+ * 追踪 console.log/warn/error/info 调用
1172
+ */
1173
+ // 保存原始方法
1174
+ const originalConsole = {
1175
+ log: console.log,
1176
+ info: console.info,
1177
+ warn: console.warn,
1178
+ error: console.error,
1179
+ };
1180
+ /**
1181
+ * 安装控制台追踪
1182
+ */
1183
+ function installConsoleTracker() {
1184
+ const cfg = config.get();
1185
+ // 默认启用控制台追踪(跟随 behavior.enabled)
1186
+ if (!cfg.behavior?.enabled) {
1187
+ return;
1188
+ }
1189
+ const levels = ['log', 'info', 'warn', 'error'];
1190
+ levels.forEach((level) => {
1191
+ console[level] = function (...args) {
1192
+ // 调用原始方法
1193
+ originalConsole[level].apply(console, args);
1194
+ // 添加到面包屑(只记录 warn 和 error)
1195
+ if (level === 'warn' || level === 'error') {
1196
+ const message = formatConsoleArgs(args);
1197
+ context.addBreadcrumb({
1198
+ type: 'console',
1199
+ category: `console.${level}`,
1200
+ data: {
1201
+ level,
1202
+ message: message.substring(0, 200), // 限制长度
1203
+ },
1204
+ });
1205
+ }
1206
+ };
1207
+ });
1208
+ if (cfg.debug) {
1209
+ originalConsole.log('[Monitor] Console tracker installed');
1210
+ }
1211
+ }
1212
+ /**
1213
+ * 格式化控制台参数
1214
+ */
1215
+ function formatConsoleArgs(args) {
1216
+ return args
1217
+ .map((arg) => {
1218
+ if (typeof arg === 'string') {
1219
+ return arg;
1220
+ }
1221
+ if (arg instanceof Error) {
1222
+ return arg.message;
1223
+ }
1224
+ try {
1225
+ return JSON.stringify(arg);
1226
+ }
1227
+ catch {
1228
+ return String(arg);
1229
+ }
1230
+ })
1231
+ .join(' ');
1232
+ }
1233
+
1234
+ /**
1235
+ * 输入事件追踪插件
1236
+ * 追踪表单输入、焦点变化等
1237
+ */
1238
+ /**
1239
+ * 安装输入追踪
1240
+ */
1241
+ function installInputTracker() {
1242
+ const cfg = config.get();
1243
+ // 默认启用输入追踪(跟随 behavior.enabled)
1244
+ if (!cfg.behavior?.enabled) {
1245
+ return;
1246
+ }
1247
+ // 监听 input 事件(使用 change 事件减少数据量)
1248
+ document.addEventListener('change', handleInputChange, true);
1249
+ // 监听焦点事件
1250
+ document.addEventListener('focus', handleFocus, true);
1251
+ document.addEventListener('blur', handleBlur, true);
1252
+ if (cfg.debug) {
1253
+ console.log('[Monitor] Input tracker installed');
1254
+ }
1255
+ }
1256
+ /**
1257
+ * 处理输入变化
1258
+ */
1259
+ function handleInputChange(event) {
1260
+ const target = event.target;
1261
+ if (!target || !isFormElement(target))
1262
+ return;
1263
+ const inputData = extractInputData(target, 'change');
1264
+ context.addBreadcrumb({
1265
+ type: 'input',
1266
+ category: 'ui.input',
1267
+ data: inputData,
1268
+ });
1269
+ }
1270
+ /**
1271
+ * 处理焦点获取
1272
+ */
1273
+ function handleFocus(event) {
1274
+ const target = event.target;
1275
+ if (!target || !isFormElement(target))
1276
+ return;
1277
+ context.addBreadcrumb({
1278
+ type: 'input',
1279
+ category: 'ui.focus',
1280
+ data: extractInputData(target, 'focus'),
1281
+ });
1282
+ }
1283
+ /**
1284
+ * 处理焦点失去
1285
+ */
1286
+ function handleBlur(event) {
1287
+ const target = event.target;
1288
+ if (!target || !isFormElement(target))
1289
+ return;
1290
+ context.addBreadcrumb({
1291
+ type: 'input',
1292
+ category: 'ui.blur',
1293
+ data: extractInputData(target, 'blur'),
1294
+ });
1295
+ }
1296
+ /**
1297
+ * 判断是否是表单元素
1298
+ */
1299
+ function isFormElement(element) {
1300
+ const tagName = element.tagName.toLowerCase();
1301
+ return ['input', 'textarea', 'select'].includes(tagName);
1302
+ }
1303
+ /**
1304
+ * 提取输入数据(脱敏处理)
1305
+ */
1306
+ function extractInputData(element, action) {
1307
+ const tagName = element.tagName.toLowerCase();
1308
+ const inputType = element.type || 'text';
1309
+ // 敏感类型不记录值
1310
+ const sensitiveTypes = ['password', 'credit-card', 'cvv'];
1311
+ sensitiveTypes.includes(inputType) ||
1312
+ element.name?.toLowerCase().includes('password') ||
1313
+ element.name?.toLowerCase().includes('secret');
1314
+ return {
1315
+ action,
1316
+ tagName,
1317
+ name: element.name || undefined,
1318
+ id: element.id || undefined,
1319
+ type: inputType,
1320
+ // 脱敏:敏感字段不记录值,其他字段只记录长度
1321
+ valueLength: element.value?.length || 0,
1322
+ hasValue: !!element.value,
1323
+ placeholder: element.placeholder || undefined,
1324
+ };
1325
+ }
1326
+
1327
+ /**
1328
+ * 行为监控插件入口
1329
+ */
1330
+ /**
1331
+ * 安装行为监控
1332
+ */
1333
+ function installBehaviorMonitor() {
1334
+ installPVTracker();
1335
+ installClickTracker();
1336
+ installRouteTracker();
1337
+ installConsoleTracker();
1338
+ installInputTracker();
1339
+ }
1340
+
1341
+ /**
1342
+ * 前端监控 SDK
1343
+ * @description 错误监控、性能监控、行为监控
1344
+ */
1345
+ // SDK 版本
1346
+ const SDK_VERSION = '1.0.0';
1347
+ /**
1348
+ * 监控 SDK 主类
1349
+ */
1350
+ class MonitorSDK {
1351
+ constructor() {
1352
+ this.initialized = false;
1353
+ }
1354
+ /**
1355
+ * 初始化 SDK
1356
+ */
1357
+ init(userConfig) {
1358
+ if (this.initialized) {
1359
+ console.warn('[Monitor] SDK already initialized');
1360
+ return;
1361
+ }
1362
+ // 初始化配置
1363
+ config.init({
1364
+ ...userConfig,
1365
+ version: userConfig.version || SDK_VERSION,
1366
+ });
1367
+ // 初始化上下文
1368
+ const maxBreadcrumbs = config.get().behavior?.maxBreadcrumbs || 20;
1369
+ context.init(maxBreadcrumbs);
1370
+ this.initialized = true;
1371
+ if (config.get().debug) {
1372
+ console.log('[Monitor] SDK initialized', config.get());
1373
+ }
1374
+ // 安装监控模块
1375
+ installErrorHandlers();
1376
+ installPerformanceMonitor();
1377
+ installBehaviorMonitor();
1378
+ }
1379
+ /**
1380
+ * 设置用户信息
1381
+ */
1382
+ setUser(user) {
1383
+ this.checkInit();
1384
+ config.setUser(user);
1385
+ }
1386
+ /**
1387
+ * 手动上报错误
1388
+ */
1389
+ captureError(error, extra) {
1390
+ this.checkInit();
1391
+ const errorData = {
1392
+ type: 'js_error',
1393
+ message: typeof error === 'string' ? error : error.message,
1394
+ stack: typeof error === 'string' ? undefined : error.stack,
1395
+ ...extra,
1396
+ };
1397
+ reporter.reportError(errorData);
1398
+ }
1399
+ /**
1400
+ * 手动上报性能数据
1401
+ */
1402
+ capturePerformance(metrics) {
1403
+ this.checkInit();
1404
+ reporter.reportPerformance({
1405
+ type: 'performance',
1406
+ metrics,
1407
+ });
1408
+ }
1409
+ /**
1410
+ * 手动上报行为数据
1411
+ */
1412
+ captureBehavior(action, data) {
1413
+ this.checkInit();
1414
+ reporter.reportBehavior({
1415
+ type: 'behavior',
1416
+ action,
1417
+ data,
1418
+ });
1419
+ }
1420
+ /**
1421
+ * 添加面包屑
1422
+ */
1423
+ addBreadcrumb(crumb) {
1424
+ this.checkInit();
1425
+ context.addBreadcrumb(crumb);
1426
+ }
1427
+ /**
1428
+ * 立即发送缓冲区数据
1429
+ */
1430
+ flush() {
1431
+ this.checkInit();
1432
+ reporter.flush();
1433
+ }
1434
+ /**
1435
+ * 获取 SDK 版本
1436
+ */
1437
+ getVersion() {
1438
+ return SDK_VERSION;
1439
+ }
1440
+ /**
1441
+ * 检查是否已初始化
1442
+ */
1443
+ checkInit() {
1444
+ if (!this.initialized) {
1445
+ throw new Error('[Monitor] SDK not initialized. Please call init() first.');
1446
+ }
1447
+ }
1448
+ }
1449
+ // 导出单例
1450
+ const monitor = new MonitorSDK();
1451
+
1452
+ exports.default = monitor;
1453
+ exports.monitor = monitor;
1454
+ //# sourceMappingURL=index.cjs.js.map