@qiu_jun/monitors 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  前端监控 SDK:采集异常 + 性能指标,上报至异常分析平台(main-server `/api/exception/report`)。
4
4
 
5
- 内置 reporter(与 main-server `exception-sdk` 协议对齐),**无外部 SDK 依赖**,可独立打包发布 npm。
6
-
7
5
  ## 安装
8
6
 
9
7
  ```bash
@@ -49,12 +47,36 @@ const monitor = new Monitor({
49
47
  |---|---|---|
50
48
  | JS 错误 | `error` | 含选择器、白屏检测 |
51
49
  | Promise 未捕获 | `unhandledrejection` | |
52
- | 资源加载失败 | `resource` | img/script/css 等 |
53
50
  | XHR 失败/超时 | `request` | message 格式 `Request failed [500]: GET /api/...` |
54
51
  | FP/FCP/FMP/LCP | `paint` | 指标在 `device` |
55
52
  | DOMContentLoaded/load | `timing` | |
56
53
  | 长任务 | `longTask` | duration ≥ 100ms 时 level=WARN |
57
54
 
55
+ ## 错误回放(可选,Web/H5)
56
+
57
+ 基于 rrweb 环形录制用户操作,**出错时**上传最近 15~30 秒回放(独立接口),错误事件通过 `device.replayId` 关联,异常平台用 rrweb-player 播放。小程序无 DOM 不支持。
58
+
59
+ ```ts
60
+ new Monitor({
61
+ // ...
62
+ replay: {
63
+ enabled: true,
64
+ sampleRate: 1, // 会话采样 0-1,默认 1
65
+ bufferSeconds: 30, // 环形缓冲长度,默认 30秒
66
+ maxUploadsPerSession: 10, // 单会话上传上限,默认 10
67
+ minUploadIntervalMs: 30000,
68
+ captureRequest: false, // 请求失败是否也触发,默认 false
69
+ maskTextClass: 'rr-mask', // 文本脱敏类名
70
+ // replayEndpoint: '...', // 默认由 endpoint 末尾 /report 换成 /replay
71
+ },
72
+ })
73
+ ```
74
+
75
+ - 仅 `error` / `unhandledrejection` 触发上传;回放走独立 `POST {endpoint 前缀}/replay`,失败静默不影响错误上报
76
+ - 同类错误采集去重:顶层 `errorDedupeMs`(默认 `0` 不启用)与 `replay.minUploadIntervalMs` 设为同值(如 `30000`)后,同一错误窗口内 1 条事件必带 1 条回放(1:1),顺带减少上报量
77
+ - 隐私默认:输入值全脱敏;敏感文本加 `.rr-mask`、敏感区域加 `.rr-block`
78
+ - 回放上传需服务端支持(契约见 [backend-integration.md](./backend-integration.md) §2.3);script 标签(UMD)接入需页面提供 rrweb / pako 的 import map,否则回放静默禁用
79
+
58
80
  ## 手动上报
59
81
 
60
82
  ```ts
@@ -68,11 +90,11 @@ monitor.capture({
68
90
  ## 构建发布
69
91
 
70
92
  ```bash
71
- pnpm build # tsc → dist/*.js + *.d.ts,vite → dist/monitor.umd.js
93
+ pnpm build # tsc → dist/*.js + *.d.ts,strip-console 剥离 console.*,vite → dist/monitor.umd.js
72
94
  npm publish
73
95
  ```
74
96
 
75
- 产物:
97
+ 产物均不含 `console.*`(调试 `console.log` 与降级 `console.warn` 一并剥离;UMD 由 vite terser `drop_console`,其余由 `scripts/strip-console.mjs`):
76
98
 
77
99
  | 路径 | 说明 |
78
100
  |---|---|
@@ -15,6 +15,31 @@ export interface IMonitorOptions {
15
15
  /** XHR 超时阈值 ms,默认 60000(1 分钟) */
16
16
  timeoutDuration?: number;
17
17
  maxQueueSize?: number;
18
+ /** 同类错误采集去重窗口(ms),默认 0 不启用;窗口内 errorType+message+filename 相同的错误只上报一条。建议与 replay.minUploadIntervalMs 同值(如 30000),错误事件与回放 1:1 */
19
+ errorDedupeMs?: number;
20
+ /** 错误回放(仅 Web/H5,小程序无 DOM 不支持),默认关闭 */
21
+ replay?: ReplayOptions;
22
+ }
23
+ /** 错误回放配置:环形录制最近操作,出错时上传最近 15~30 秒,经独立接口 + device.replayId 关联异常事件 */
24
+ export interface ReplayOptions {
25
+ /** 开启回放,默认 false */
26
+ enabled?: boolean;
27
+ /** 会话采样率 0-1,默认 1(命中才录制) */
28
+ sampleRate?: number;
29
+ /** 环形缓冲长度(秒),默认 30(实际覆盖 bufferSeconds/2 ~ bufferSeconds) */
30
+ bufferSeconds?: number;
31
+ /** 单会话回放上传上限,默认 10 */
32
+ maxUploadsPerSession?: number;
33
+ /** 两次上传最小间隔(ms),默认 30000 */
34
+ minUploadIntervalMs?: number;
35
+ /** 单条回放解压前体积上限(字节),默认 2MB,超出只保留最新快照段 */
36
+ maxReplayBytes?: number;
37
+ /** 请求失败是否也触发回放上传,默认 false(仅 error/unhandledrejection 触发) */
38
+ captureRequest?: boolean;
39
+ /** 文本脱敏类名,默认 'rr-mask';输入值默认全脱敏(maskAllInputs) */
40
+ maskTextClass?: string;
41
+ /** 回放上传地址;默认由 endpoint 末尾 /report 换成 /replay,推导不出时必须显式传 */
42
+ replayEndpoint?: string;
18
43
  }
19
44
  export interface BaseLog {
20
45
  title: string;
@@ -26,14 +51,13 @@ export interface BaseLog {
26
51
  }
27
52
  export interface ErrorLog {
28
53
  type: "error";
29
- errorType: "jsError" | "promiseError" | "loadResourceError" | "xhrError";
54
+ errorType: "jsError" | "promiseError" | "xhrError";
30
55
  message: string;
31
56
  filename?: string;
32
57
  position?: string;
33
58
  stack?: string;
34
59
  selector?: string;
35
60
  isWhiteScreen?: boolean;
36
- tagName?: string;
37
61
  xhrData?: {
38
62
  eventType: "load" | "error";
39
63
  url: string;
@@ -1,91 +1,45 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = onWindowErr;
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: !0
5
+ }), exports.default = onWindowErr;
6
+
4
7
  const utils_1 = require("../utils");
8
+
5
9
  function onWindowErr() {
6
- window.addEventListener('error', event => {
7
- console.log("error event: ", event);
8
- // 监听 JS/CSS 资源文件加载错误
9
- const target = event.target;
10
- let filename;
11
- if (target &&
12
- (filename =
13
- target.src || target.href)) {
14
- // 1、数据建模存储
15
- const log = {
16
- type: "error",
17
- errorType: "loadResourceError", // 错误类型 - JS/CSS 资源加载错误
18
- message: `${filename} resource loading fail.`,
19
- filename, // 报错的文件
20
- tagName: target.tagName, // 资源标签名称
21
- // body script
22
- selector: (0, utils_1.getSelector)(event.target),
23
- };
24
- console.log("log: ", log);
25
- // 2、上报数据
26
- // @ts-ignore
27
- this.send(log);
28
- }
29
- else {
30
- // 监听 JS 代码执行出错
31
- const lastEvent = (0, utils_1.getLastEvent)(); // 监听到错误后,获取到最后一个交互事件
32
- const isWhiteScreen = (0, utils_1.checkWhiteScreen)(); // 检查是否白屏
33
- // 1.1、数据建模存储
34
- const errorLog = {
35
- // kind: "stability", // 监控指标的大类
36
- type: "error",
37
- errorType: "jsError",
38
- message: event.message,
39
- filename: event.filename,
40
- position: `${event.lineno}:${event.colno}`,
41
- stack: (0, utils_1.formatStack)(event.error.stack),
42
- selector: lastEvent ? (0, utils_1.getSelector)() : "",
43
- isWhiteScreen,
44
- };
45
- console.log("js error log: ", errorLog);
46
- // 1.2、上报数据
47
- // @ts-ignore
48
- this.send(errorLog);
49
- }
50
- }, true);
51
- // promise
52
- window.addEventListener('unhandledrejection', event => {
53
- console.log("Promise error event: ", event);
54
- const lastEvent = (0, utils_1.getLastEvent)(); // 监听到错误后,获取到最后一个交互事件
55
- let message;
56
- const reason = event.reason; // Promise 失败的原因
57
- let filename;
58
- let line = 0;
59
- let column = 0;
60
- let stack = "";
61
- if (typeof event.reason === "string") {
62
- // 情况 1、是 Promise reject 抛出的错误(没有办法获取 stack 等信息)
63
- message = reason;
64
- }
65
- else if (typeof reason === "object") {
66
- message = reason.message;
67
- // 情况 2、是 Promise 中 JS 代码执行出错
68
- if (reason.stack) {
69
- const matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
70
- filename = matchResult[1];
71
- line = matchResult[2];
72
- column = matchResult[3];
73
- stack = (0, utils_1.formatStack)(reason.stack);
74
- }
75
- }
76
- // 2.1、数据建模存储
77
- const errorLog = {
78
- type: "error",
79
- errorType: "promiseError", // 错误类型 - Promise 代码错误
80
- message,
81
- filename,
82
- position: `${line}:${column}`,
83
- stack,
84
- selector: lastEvent ? (0, utils_1.getSelector)() : "",
85
- };
86
- console.log("promise error log: ", errorLog);
87
- // 2.2、上报数据
88
- // @ts-ignore
89
- this.send(errorLog);
90
- }, true);
91
- }
10
+ window.addEventListener("error", e => {
11
+ const t = e.target;
12
+ if (t && (t.src || t.href)) return;
13
+ const s = (0, utils_1.getLastEvent)(), r = (0, utils_1.checkWhiteScreen)(), o = {
14
+ type: "error",
15
+ errorType: "jsError",
16
+ message: e.message,
17
+ filename: e.filename,
18
+ position: `${e.lineno}:${e.colno}`,
19
+ stack: (0, utils_1.formatStack)(e.error.stack),
20
+ selector: s ? (0, utils_1.getSelector)() : "",
21
+ isWhiteScreen: r
22
+ };
23
+ this.send(o);
24
+ }, !0), window.addEventListener("unhandledrejection", e => {
25
+ const t = (0, utils_1.getLastEvent)();
26
+ let s;
27
+ const r = e.reason;
28
+ let o, n = 0, i = 0, c = "";
29
+ if ("string" == typeof e.reason) s = r; else if ("object" == typeof r && (s = r.message,
30
+ r.stack)) {
31
+ const e = r.stack.match(/at\s+(.+):(\d+):(\d+)/);
32
+ o = e[1], n = e[2], i = e[3], c = (0, utils_1.formatStack)(r.stack);
33
+ }
34
+ const a = {
35
+ type: "error",
36
+ errorType: "promiseError",
37
+ message: s,
38
+ filename: o,
39
+ position: `${n}:${i}`,
40
+ stack: c,
41
+ selector: t ? (0, utils_1.getSelector)() : ""
42
+ };
43
+ this.send(a);
44
+ }, !0);
45
+ }
@@ -1,27 +1,28 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = onLongTask;
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: !0
5
+ }), exports.default = onLongTask;
6
+
4
7
  const utils_1 = require("../utils");
8
+
5
9
  function onLongTask() {
6
- if (PerformanceObserver) {
7
- const observerLongTask = new PerformanceObserver(list => {
8
- list.getEntries().forEach(entry => {
9
- // 执行时长大于 100 ms
10
- if (entry.duration > 100) {
11
- const lastEvent = (0, utils_1.getLastEvent)();
12
- const log = {
13
- type: "longTask",
14
- startTime: entry.startTime, // 开始时间
15
- duration: entry.duration, // 持续时间
16
- selector: lastEvent ? (0, utils_1.getSelector)() : "",
17
- eventType: lastEvent === null || lastEvent === void 0 ? void 0 : lastEvent.type,
18
- };
19
- console.log("longTask log: ", log);
20
- // @ts-ignore
21
- this.send(log);
22
- }
23
- });
24
- });
25
- observerLongTask.observe({ entryTypes: ["longtask"] });
26
- }
27
- }
10
+ if (PerformanceObserver) {
11
+ new PerformanceObserver(e => {
12
+ e.getEntries().forEach(e => {
13
+ if (e.duration > 100) {
14
+ const t = (0, utils_1.getLastEvent)(), r = {
15
+ type: "longTask",
16
+ startTime: e.startTime,
17
+ duration: e.duration,
18
+ selector: t ? (0, utils_1.getSelector)() : "",
19
+ eventType: null == t ? void 0 : t.type
20
+ };
21
+ this.send(r);
22
+ }
23
+ });
24
+ }).observe({
25
+ entryTypes: [ "longtask" ]
26
+ });
27
+ }
28
+ }
@@ -1,83 +1,45 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = onPaint;
4
- /** 带历史缓冲的 PerformanceObserver(兼容 SDK 晚于首次绘制才启动) */
5
- function observeBuffered(type, callback) {
6
- if (typeof PerformanceObserver === 'undefined')
7
- return null;
8
- try {
9
- const observer = new PerformanceObserver((list) => {
10
- callback(list.getEntries());
11
- });
12
- // type + buffered 可拿到观察前已产生的条目
13
- observer.observe({ type, buffered: true });
14
- return observer;
15
- }
16
- catch (err) {
17
- console.warn('[monitor] PerformanceObserver 不支持 type=', type, err);
18
- return null;
19
- }
2
+
3
+ function observeBuffered(e, n) {
4
+ if ("undefined" == typeof PerformanceObserver) return null;
5
+ try {
6
+ const t = new PerformanceObserver(e => {
7
+ n(e.getEntries());
8
+ });
9
+ return t.observe({
10
+ type: e,
11
+ buffered: !0
12
+ }), t;
13
+ } catch (e) {
14
+ return null;
15
+ }
20
16
  }
21
- /**
22
- * 采集绘制指标 FP/FCP/FMP/LCP。
23
- * 若 SDK 在 window.load 之后初始化,仍会延迟上报(含 buffered 历史指标)。
24
- */
17
+
25
18
  function onPaint() {
26
- if (typeof PerformanceObserver === 'undefined') {
27
- console.warn('[monitor] 当前环境不支持 PerformanceObserver,跳过 paint 采集');
28
- return;
29
- }
30
- let FP = null;
31
- let FCP = null;
32
- let FMP = null;
33
- let LCP = null;
34
- const observerFPAndFCP = observeBuffered('paint', (entries) => {
35
- for (const entry of entries) {
36
- if (entry.name === 'first-paint') {
37
- FP = entry;
38
- console.log('[monitor] FP', FP.startTime);
39
- }
40
- else if (entry.name === 'first-contentful-paint') {
41
- FCP = entry;
42
- console.log('[monitor] FCP', FCP.startTime);
43
- }
44
- }
45
- });
46
- const observerFMP = observeBuffered('element', (entries) => {
47
- if (entries.length > 0) {
48
- FMP = entries[0];
49
- console.log('[monitor] FMP', FMP.startTime);
50
- observerFMP === null || observerFMP === void 0 ? void 0 : observerFMP.disconnect();
51
- }
52
- });
53
- // LCP 会多次更新,上报前再 disconnect
54
- const observerLCP = observeBuffered('largest-contentful-paint', (entries) => {
55
- if (entries.length > 0) {
56
- LCP = entries[entries.length - 1];
57
- console.log('[monitor] LCP', LCP.startTime);
58
- }
59
- });
60
- /** load 后再等一段时间,尽量拿到稳定 LCP 再上报 */
61
- const scheduleReport = () => {
62
- setTimeout(() => {
63
- observerLCP === null || observerLCP === void 0 ? void 0 : observerLCP.disconnect();
64
- observerFPAndFCP === null || observerFPAndFCP === void 0 ? void 0 : observerFPAndFCP.disconnect();
65
- observerFMP === null || observerFMP === void 0 ? void 0 : observerFMP.disconnect();
66
- const log = {
67
- type: 'paint',
68
- FP: FP === null || FP === void 0 ? void 0 : FP.startTime,
69
- FCP: FCP === null || FCP === void 0 ? void 0 : FCP.startTime,
70
- FMP: FMP === null || FMP === void 0 ? void 0 : FMP.startTime,
71
- LCP: LCP === null || LCP === void 0 ? void 0 : LCP.startTime,
72
- };
73
- console.log('[monitor] paint 上报', log);
74
- this.send(log);
75
- }, 3000);
76
- };
77
- if (document.readyState === 'complete') {
78
- console.log('[monitor] 页面已 load 完成,延迟补报 paint');
79
- scheduleReport();
80
- return;
81
- }
82
- window.addEventListener('load', scheduleReport);
19
+ if ("undefined" == typeof PerformanceObserver) return;
20
+ let e = null, n = null, t = null, r = null;
21
+ const l = observeBuffered("paint", t => {
22
+ for (const r of t) "first-paint" === r.name ? e = r : "first-contentful-paint" === r.name && (n = r);
23
+ }), o = observeBuffered("element", e => {
24
+ e.length > 0 && (t = e[0], null == o || o.disconnect());
25
+ }), u = observeBuffered("largest-contentful-paint", e => {
26
+ e.length > 0 && (r = e[e.length - 1]);
27
+ }), i = () => {
28
+ setTimeout(() => {
29
+ null == u || u.disconnect(), null == l || l.disconnect(), null == o || o.disconnect();
30
+ const i = {
31
+ type: "paint",
32
+ FP: null == e ? void 0 : e.startTime,
33
+ FCP: null == n ? void 0 : n.startTime,
34
+ FMP: null == t ? void 0 : t.startTime,
35
+ LCP: null == r ? void 0 : r.startTime
36
+ };
37
+ this.send(i);
38
+ }, 3e3);
39
+ };
40
+ "complete" !== document.readyState ? window.addEventListener("load", i) : i();
83
41
  }
42
+
43
+ Object.defineProperty(exports, "__esModule", {
44
+ value: !0
45
+ }), exports.default = onPaint;
@@ -1,43 +1,30 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = onTiming;
4
- /** 采集并上报页面加载时序(DOMContentLoaded / load) */
2
+
5
3
  function reportTiming() {
6
- let DOMContentLoadedTime = 0;
7
- let loadTime = 0;
8
- // 新版:PerformanceNavigationTiming;旧版降级 performance.timing
9
- if (performance.getEntriesByType) {
10
- const perfEntries = performance.getEntriesByType('navigation');
11
- if (perfEntries.length > 0) {
12
- const navigationEntry = perfEntries[0];
13
- const { domContentLoadedEventStart, loadEventStart, fetchStart } = navigationEntry;
14
- DOMContentLoadedTime = domContentLoadedEventStart - fetchStart;
15
- loadTime = loadEventStart - fetchStart;
16
- }
4
+ let t = 0, e = 0;
5
+ if (performance.getEntriesByType) {
6
+ const n = performance.getEntriesByType("navigation");
7
+ if (n.length > 0) {
8
+ const o = n[0], {domContentLoadedEventStart: i, loadEventStart: r, fetchStart: a} = o;
9
+ t = i - a, e = r - a;
17
10
  }
18
- else {
19
- const { fetchStart, domContentLoadedEventStart, loadEventStart } = performance.timing;
20
- DOMContentLoadedTime = domContentLoadedEventStart - fetchStart;
21
- loadTime = loadEventStart - fetchStart;
22
- }
23
- const log = {
24
- type: 'timing',
25
- DOMContentLoadedTime,
26
- loadTime,
27
- };
28
- console.log('[monitor] timing 上报', log);
29
- this.send(log);
11
+ } else {
12
+ const {fetchStart: n, domContentLoadedEventStart: o, loadEventStart: i} = performance.timing;
13
+ t = o - n, e = i - n;
14
+ }
15
+ const n = {
16
+ type: "timing",
17
+ DOMContentLoadedTime: t,
18
+ loadTime: e
19
+ };
20
+ this.send(n);
30
21
  }
31
- /**
32
- * 监听页面加载时序。
33
- * 若 SDK 在 window.load 之后才初始化,直接补报,避免永远收不到 timing。
34
- */
22
+
35
23
  function onTiming() {
36
- const run = () => reportTiming.call(this);
37
- if (document.readyState === 'complete') {
38
- console.log('[monitor] 页面已 load 完成,立即补报 timing');
39
- run();
40
- return;
41
- }
42
- window.addEventListener('load', run);
24
+ const t = () => reportTiming.call(this);
25
+ "complete" !== document.readyState ? window.addEventListener("load", t) : t();
43
26
  }
27
+
28
+ Object.defineProperty(exports, "__esModule", {
29
+ value: !0
30
+ }), exports.default = onTiming;
package/dist/monitor.d.ts CHANGED
@@ -12,7 +12,10 @@ export default class Monitor {
12
12
  maxQueueSize: number;
13
13
  sendTimer: number | null;
14
14
  reporter: ExcReporter;
15
+ private replay;
15
16
  private readonly options;
17
+ /** errorDedupeMs 去重状态:errorType|message|filename → 上次放行时间 */
18
+ private errorSentAt;
16
19
  originalXHR: {
17
20
  new (): XMLHttpRequest;
18
21
  prototype: XMLHttpRequest;
@@ -29,7 +32,11 @@ export default class Monitor {
29
32
  originalOnerror: OnErrorEventHandler;
30
33
  constructor(options: IMonitorOptions);
31
34
  init(): void;
32
- /** 统一上报:转异常平台格式 → reporter 批量 POST */
35
+ /** 回放初始化:显式开启 + 采样命中 + 回放地址可解析 */
36
+ private initReplay;
37
+ /** errorDedupeMs 窗口内同类错误(errorType+message+filename 相同)直接丢弃,连回放触发一并跳过;只在放行时刷新时间戳(非滑动窗口),与 minUploadIntervalMs 的节奏一致 */
38
+ private isDuplicateError;
39
+ /** 统一上报:转异常平台格式 → reporter 批量 POST;错误触发回放切片上传并回填 device.replayId */
33
40
  send(data: MonitorTypeLog): void;
34
41
  /** 手动上报自定义事件 */
35
42
  capture(event: ExcEventPayload): void;