@qiu_jun/monitors 0.0.7 → 0.0.9

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
@@ -47,14 +47,23 @@ const monitor = new Monitor({
47
47
  |---|---|---|
48
48
  | JS 错误 | `error` | 含选择器、白屏检测 |
49
49
  | Promise 未捕获 | `unhandledrejection` | |
50
- | XHR 失败/超时 | `request` | message 格式 `Request failed [500]: GET /api/...` |
50
+ | XHR/fetch 失败/超时 | `request` | 网络失败 `[network]`、HTTP `[500]`;业务错误 `[business 1102]`(需配 `isBusinessError`,见下) |
51
51
  | FP/FCP/FMP/LCP | `paint` | 指标在 `device` |
52
52
  | DOMContentLoaded/load | `timing` | |
53
53
  | 长任务 | `longTask` | duration ≥ 100ms 时 level=WARN |
54
54
 
55
+ **业务错误拦截**:后端 HTTP 200 但响应体 `code` 非成功(如 `{ code: 1102, message: '无权限访问' }`)默认不会上报,配置判定函数后按 `request` 事件上报(`message` 形如 `Request failed [business 1102]: POST /api/xxx`,`device.msg` 带后端 message、`device.code` 带错误码):
56
+
57
+ ```ts
58
+ new Monitor({
59
+ // ...
60
+ isBusinessError: body => (body as { code?: number })?.code !== 0, // 项目内约定 code !== 0 即业务错误
61
+ })
62
+ ```
63
+
55
64
  ## 错误回放(可选,Web/H5)
56
65
 
57
- 基于 rrweb 环形录制用户操作,**出错时**上传最近 30~60 秒回放(独立接口),错误事件通过 `device.replayId` 关联,异常平台用 rrweb-player 播放。小程序无 DOM 不支持。
66
+ 基于 rrweb 环形录制用户操作,**出错时**上传最近 15~30 秒回放(独立接口),错误事件通过 `device.replayId` 关联,异常平台用 rrweb-player 播放。小程序无 DOM 不支持。
58
67
 
59
68
  ```ts
60
69
  new Monitor({
@@ -62,9 +71,9 @@ new Monitor({
62
71
  replay: {
63
72
  enabled: true,
64
73
  sampleRate: 1, // 会话采样 0-1,默认 1
65
- bufferSeconds: 60, // 环形缓冲长度,默认 60
74
+ bufferSeconds: 30, // 环形缓冲长度,默认 30秒
66
75
  maxUploadsPerSession: 10, // 单会话上传上限,默认 10
67
- minUploadIntervalMs: 30_000,
76
+ minUploadIntervalMs: 30000,
68
77
  captureRequest: false, // 请求失败是否也触发,默认 false
69
78
  maskTextClass: 'rr-mask', // 文本脱敏类名
70
79
  // replayEndpoint: '...', // 默认由 endpoint 末尾 /report 换成 /replay
@@ -73,6 +82,7 @@ new Monitor({
73
82
  ```
74
83
 
75
84
  - 仅 `error` / `unhandledrejection` 触发上传;回放走独立 `POST {endpoint 前缀}/replay`,失败静默不影响错误上报
85
+ - 同类错误采集去重:顶层 `errorDedupeMs`(默认 `0` 不启用)与 `replay.minUploadIntervalMs` 设为同值(如 `30000`)后,同一错误窗口内 1 条事件必带 1 条回放(1:1),顺带减少上报量
76
86
  - 隐私默认:输入值全脱敏;敏感文本加 `.rr-mask`、敏感区域加 `.rr-block`
77
87
  - 回放上传需服务端支持(契约见 [backend-integration.md](./backend-integration.md) §2.3);script 标签(UMD)接入需页面提供 rrweb / pako 的 import map,否则回放静默禁用
78
88
 
@@ -89,11 +99,11 @@ monitor.capture({
89
99
  ## 构建发布
90
100
 
91
101
  ```bash
92
- pnpm build # tsc → dist/*.js + *.d.ts,vite → dist/monitor.umd.js
102
+ pnpm build # tsc → dist/*.js + *.d.ts,strip-console 剥离 console.*,vite → dist/monitor.umd.js
93
103
  npm publish
94
104
  ```
95
105
 
96
- 产物:
106
+ 产物均不含 `console.*`(调试 `console.log` 与降级 `console.warn` 一并剥离;UMD 由 vite terser `drop_console`,其余由 `scripts/strip-console.mjs`):
97
107
 
98
108
  | 路径 | 说明 |
99
109
  |---|---|
@@ -8,7 +8,7 @@ export declare class ExcReporter {
8
8
  private timer;
9
9
  private sending;
10
10
  constructor(options: ExcSdkOptions, transport: ExcTransport);
11
- /** 采样 + 入缓冲 */
11
+ /** 采样 + 入缓冲:ERROR/FATAL 走 errorSampleRate(默认全量),WARN 及以下走 sampleRate */
12
12
  capture(event: ExcEventPayload): void;
13
13
  flush(): Promise<void>;
14
14
  destroy(): void;
@@ -40,15 +40,17 @@ class ExcReporter {
40
40
  value: false
41
41
  });
42
42
  }
43
- /** 采样 + 入缓冲 */
43
+ /** 采样 + 入缓冲:ERROR/FATAL 走 errorSampleRate(默认全量),WARN 及以下走 sampleRate */
44
44
  capture(event) {
45
45
  var _a, _b, _c, _d, _e;
46
- const sampleRate = (_a = this.options.sampleRate) !== null && _a !== void 0 ? _a : 100;
46
+ const level = (_a = event.level) !== null && _a !== void 0 ? _a : 'ERROR';
47
+ const isErrorLevel = level === 'ERROR' || level === 'FATAL';
48
+ const sampleRate = (_b = (isErrorLevel ? this.options.errorSampleRate : this.options.sampleRate)) !== null && _b !== void 0 ? _b : 100;
47
49
  if (sampleRate < 100 && Math.random() * 100 >= sampleRate)
48
50
  return;
49
51
  this.buffer.push({
50
52
  ...event,
51
- level: (_b = event.level) !== null && _b !== void 0 ? _b : 'ERROR',
53
+ level,
52
54
  occurredAt: (_c = event.occurredAt) !== null && _c !== void 0 ? _c : new Date().toISOString(),
53
55
  sdkVersion: (_d = event.sdkVersion) !== null && _d !== void 0 ? _d : exports.SDK_VERSION,
54
56
  });
@@ -8,7 +8,10 @@ export interface ExcSdkOptions {
8
8
  projectKey: string;
9
9
  release?: string;
10
10
  environment?: string;
11
+ /** 低级别事件(WARN/INFO,性能类等)采样率(%),0-100,默认 100 全量 */
11
12
  sampleRate?: number;
13
+ /** 错误事件(ERROR/FATAL)采样率(%),0-100,默认 100 全量;不随 sampleRate 联动,需压错误量时显式调低 */
14
+ errorSampleRate?: number;
12
15
  maxBatch?: number;
13
16
  flushInterval?: number;
14
17
  }
@@ -15,16 +15,20 @@ 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
+ /** 业务错误判定:HTTP 2xx 但响应体 code 非成功的接口(如 { code: 1102, message: '无权限访问' })返回 true 则上报为 request 事件。入参为解析后的响应体(非 JSON / 解析失败跳过);XHR responseType 为 blob/arraybuffer 时读不到响应体同样跳过。示例:body => (body as any)?.code !== 0 */
21
+ isBusinessError?: (body: unknown) => boolean;
18
22
  /** 错误回放(仅 Web/H5,小程序无 DOM 不支持),默认关闭 */
19
23
  replay?: ReplayOptions;
20
24
  }
21
- /** 错误回放配置:环形录制最近操作,出错时上传最近 30~60 秒,经独立接口 + device.replayId 关联异常事件 */
25
+ /** 错误回放配置:环形录制最近操作,出错时上传最近 15~30 秒,经独立接口 + device.replayId 关联异常事件 */
22
26
  export interface ReplayOptions {
23
27
  /** 开启回放,默认 false */
24
28
  enabled?: boolean;
25
29
  /** 会话采样率 0-1,默认 1(命中才录制) */
26
30
  sampleRate?: number;
27
- /** 环形缓冲长度(秒),默认 60(实际覆盖 bufferSeconds/2 ~ bufferSeconds) */
31
+ /** 环形缓冲长度(秒),默认 30(实际覆盖 bufferSeconds/2 ~ bufferSeconds) */
28
32
  bufferSeconds?: number;
29
33
  /** 单会话回放上传上限,默认 10 */
30
34
  maxUploadsPerSession?: number;
@@ -95,6 +99,11 @@ export interface RequestLog {
95
99
  status?: number;
96
100
  url?: string;
97
101
  method?: string;
102
+ /** 业务错误详情(isBusinessError 判定命中时填充,如 { code: 1102, message: '无权限访问' }) */
103
+ business?: {
104
+ code?: number | string;
105
+ message?: string;
106
+ };
98
107
  /** 请求发生时页面地址(captureRequest 自动填充) */
99
108
  page?: string;
100
109
  }
@@ -50,6 +50,8 @@ export interface MiniProgramMonitorOptions extends ExcSdkOptions {
50
50
  interceptLifecycle?: boolean;
51
51
  /** 拦截 wx.request 上报失败请求(默认 true,上报自身 URL 除外,防递归) */
52
52
  interceptRequest?: boolean;
53
+ /** 业务错误判定:statusCode 2xx 但响应体 code 非成功时上报,判定方式同 H5 侧 IMonitorOptions.isBusinessError */
54
+ isBusinessError?: (body: unknown) => boolean;
53
55
  }
54
56
  export interface MiniProgramMonitor {
55
57
  reporter: ExcReporter;
@@ -10,6 +10,7 @@ exports.createMiniProgramMonitor = createMiniProgramMonitor;
10
10
  const reporter_1 = require("./exception/reporter");
11
11
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return reporter_1.SDK_VERSION; } });
12
12
  Object.defineProperty(exports, "createReporter", { enumerable: true, get: function () { return reporter_1.createReporter; } });
13
+ const businessError_1 = require("./utils/businessError");
13
14
  /** 从全局取 wx(微信 / uni-app 等均已注入),取不到返回 undefined */
14
15
  function getWx() {
15
16
  return globalThis.wx;
@@ -143,7 +144,7 @@ function currentPageRoute() {
143
144
  }
144
145
  }
145
146
  /** 拦截 wx.request:失败的请求按 H5 侧 request 事件格式上报(上报自身 URL 除外,防递归) */
146
- function interceptRequest(wxApi, endpoint, capture) {
147
+ function interceptRequest(wxApi, endpoint, isBusinessError, capture) {
147
148
  const original = wxApi.request.bind(wxApi);
148
149
  wxApi.request = (options) => {
149
150
  var _a;
@@ -154,7 +155,7 @@ function interceptRequest(wxApi, endpoint, capture) {
154
155
  return original({
155
156
  ...options,
156
157
  success: (res) => {
157
- var _a, _b;
158
+ var _a, _b, _c;
158
159
  if (res.statusCode >= 400) {
159
160
  capture({
160
161
  type: 'request',
@@ -162,7 +163,18 @@ function interceptRequest(wxApi, endpoint, capture) {
162
163
  device: { durationMs: Date.now() - start },
163
164
  });
164
165
  }
165
- (_b = options === null || options === void 0 ? void 0 : options.success) === null || _b === void 0 ? void 0 : _b.call(options, res);
166
+ else if (isBusinessError) {
167
+ // 业务错误:statusCode 2xx 但响应体 code 非成功(res.data 默认已被 wx 按 JSON 解析)
168
+ const business = (0, businessError_1.detectBusinessError)(res.data, isBusinessError);
169
+ if (business) {
170
+ capture({
171
+ type: 'request',
172
+ message: `Request failed [business${business.code !== undefined ? ` ${business.code}` : ''}]: ${String((_b = options === null || options === void 0 ? void 0 : options.method) !== null && _b !== void 0 ? _b : 'GET').toUpperCase()} ${url}`,
173
+ device: { durationMs: Date.now() - start, msg: business.message, code: business.code },
174
+ });
175
+ }
176
+ }
177
+ (_c = options === null || options === void 0 ? void 0 : options.success) === null || _c === void 0 ? void 0 : _c.call(options, res);
166
178
  },
167
179
  fail: (err) => {
168
180
  var _a, _b;
@@ -247,7 +259,7 @@ function createMiniProgramMonitor(options) {
247
259
  device: { lifecycle: info.lifecycle, duration: info.duration, component: info.component },
248
260
  });
249
261
  const restoreConstructors = options.interceptLifecycle !== false ? interceptConstructors() : null;
250
- const restoreRequest = options.interceptRequest !== false ? interceptRequest(wxApi, options.endpoint, doCapture) : null;
262
+ const restoreRequest = options.interceptRequest !== false ? interceptRequest(wxApi, options.endpoint, options.isBusinessError, doCapture) : null;
251
263
  return {
252
264
  reporter,
253
265
  capture: doCapture,
@@ -1,74 +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
- // 资源加载失败(target 带 src/href,如 img/script/link)噪声多、价值低,不再上报
9
- const target = event.target;
10
- if (target &&
11
- (target.src || target.href)) {
12
- return;
13
- }
14
- // 监听 JS 代码执行出错
15
- const lastEvent = (0, utils_1.getLastEvent)(); // 监听到错误后,获取到最后一个交互事件
16
- const isWhiteScreen = (0, utils_1.checkWhiteScreen)(); // 检查是否白屏
17
- // 1.1、数据建模存储
18
- const errorLog = {
19
- // kind: "stability", // 监控指标的大类
20
- type: "error",
21
- errorType: "jsError",
22
- message: event.message,
23
- filename: event.filename,
24
- position: `${event.lineno}:${event.colno}`,
25
- stack: (0, utils_1.formatStack)(event.error.stack),
26
- selector: lastEvent ? (0, utils_1.getSelector)() : "",
27
- isWhiteScreen,
28
- };
29
- console.log("js error log: ", errorLog);
30
- // 1.2、上报数据
31
- // @ts-ignore
32
- this.send(errorLog);
33
- }, true);
34
- // promise
35
- window.addEventListener('unhandledrejection', event => {
36
- console.log("Promise error event: ", event);
37
- const lastEvent = (0, utils_1.getLastEvent)(); // 监听到错误后,获取到最后一个交互事件
38
- let message;
39
- const reason = event.reason; // Promise 失败的原因
40
- let filename;
41
- let line = 0;
42
- let column = 0;
43
- let stack = "";
44
- if (typeof event.reason === "string") {
45
- // 情况 1、是 Promise reject 抛出的错误(没有办法获取 stack 等信息)
46
- message = reason;
47
- }
48
- else if (typeof reason === "object") {
49
- message = reason.message;
50
- // 情况 2、是 Promise 中 JS 代码执行出错
51
- if (reason.stack) {
52
- const matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
53
- filename = matchResult[1];
54
- line = matchResult[2];
55
- column = matchResult[3];
56
- stack = (0, utils_1.formatStack)(reason.stack);
57
- }
58
- }
59
- // 2.1、数据建模存储
60
- const errorLog = {
61
- type: "error",
62
- errorType: "promiseError", // 错误类型 - Promise 代码错误
63
- message,
64
- filename,
65
- position: `${line}:${column}`,
66
- stack,
67
- selector: lastEvent ? (0, utils_1.getSelector)() : "",
68
- };
69
- console.log("promise error log: ", errorLog);
70
- // 2.2、上报数据
71
- // @ts-ignore
72
- this.send(errorLog);
73
- }, true);
74
- }
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
+ }
@@ -0,0 +1 @@
1
+ export default function onFetch(): void;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = onFetch;
4
+ const businessError_1 = require("../utils/businessError");
5
+ /** 主动取消/超时产生的异常名,不算请求失败 */
6
+ const SKIPPED_ERROR_NAMES = ['AbortError', 'TimeoutError'];
7
+ function onFetch() {
8
+ const originalFetch = typeof window.fetch === 'function' ? window.fetch.bind(window) : undefined;
9
+ // @ts-ignore
10
+ const _this = this;
11
+ if (!originalFetch)
12
+ return;
13
+ /** 监控自身的异常不能影响业务请求 */
14
+ // @ts-ignore
15
+ const safeCapture = (info) => {
16
+ try {
17
+ _this.captureRequest(info);
18
+ }
19
+ catch ( /* 静默 */_a) { /* 静默 */ }
20
+ };
21
+ window.fetch = function (input, init) {
22
+ var _a;
23
+ const reqUrl = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url);
24
+ const reqMethod = String((_a = init === null || init === void 0 ? void 0 : init.method) !== null && _a !== void 0 ? _a : (input instanceof Request ? input.method : 'GET')).toUpperCase();
25
+ // 上报/回放自身 URL 不拦截:防递归,且 clone 读 body 会干扰 keepalive 上报
26
+ if ((_this.endpoint && reqUrl === _this.endpoint) || (_this.replayEndpoint && reqUrl === _this.replayEndpoint))
27
+ return originalFetch(input, init);
28
+ const startTime = new Date().getTime();
29
+ return originalFetch(input, init).then((res) => {
30
+ var _a;
31
+ const duration = new Date().getTime() - startTime;
32
+ if (res.status >= 400) {
33
+ safeCapture({
34
+ type: 'fetch',
35
+ url: reqUrl,
36
+ method: reqMethod,
37
+ msg: `fetch请求失败: 状态码 ${res.status}`,
38
+ duration,
39
+ status: res.status,
40
+ success: false,
41
+ startTime,
42
+ });
43
+ }
44
+ else if (res.status >= 200 && res.status < 300 && _this.isBusinessError) {
45
+ // 仅对 JSON/文本类响应 clone 读文本判定业务错误,避免缓冲大体积二进制;clone 不消费业务侧响应体
46
+ const contentType = (_a = res.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
47
+ if (contentType.includes('json') || contentType.startsWith('text/')) {
48
+ res.clone().text().then((text) => {
49
+ var _a;
50
+ const business = (0, businessError_1.detectBusinessError)(text, _this.isBusinessError);
51
+ if (!business)
52
+ return;
53
+ safeCapture({
54
+ type: 'fetch',
55
+ url: reqUrl,
56
+ method: reqMethod,
57
+ msg: `fetch业务错误: ${(_a = business.message) !== null && _a !== void 0 ? _a : reqUrl}`,
58
+ duration: new Date().getTime() - startTime,
59
+ status: res.status,
60
+ success: false,
61
+ business,
62
+ startTime,
63
+ });
64
+ }).catch(() => { });
65
+ }
66
+ }
67
+ return res;
68
+ }, (err) => {
69
+ var _a;
70
+ if (SKIPPED_ERROR_NAMES.includes((_a = err === null || err === void 0 ? void 0 : err.name) !== null && _a !== void 0 ? _a : ''))
71
+ throw err;
72
+ safeCapture({
73
+ type: 'fetch',
74
+ url: reqUrl,
75
+ method: reqMethod,
76
+ msg: `fetch请求错误: ${reqUrl}`,
77
+ duration: new Date().getTime() - startTime,
78
+ status: 0,
79
+ success: false,
80
+ startTime,
81
+ });
82
+ throw err;
83
+ });
84
+ };
85
+ }
@@ -3,3 +3,4 @@ export { default as onLongTask } from './longTask';
3
3
  export { default as onPaint } from './paint';
4
4
  export { default as onTiming } from './timing';
5
5
  export { default as onXHR } from './xhr';
6
+ export { default as onFetch } from './fetch';
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.onXHR = exports.onTiming = exports.onPaint = exports.onLongTask = exports.onWindowErr = void 0;
6
+ exports.onFetch = exports.onXHR = exports.onTiming = exports.onPaint = exports.onLongTask = exports.onWindowErr = void 0;
7
7
  var err_1 = require("./err");
8
8
  Object.defineProperty(exports, "onWindowErr", { enumerable: true, get: function () { return __importDefault(err_1).default; } });
9
9
  var longTask_1 = require("./longTask");
@@ -14,3 +14,5 @@ var timing_1 = require("./timing");
14
14
  Object.defineProperty(exports, "onTiming", { enumerable: true, get: function () { return __importDefault(timing_1).default; } });
15
15
  var xhr_1 = require("./xhr");
16
16
  Object.defineProperty(exports, "onXHR", { enumerable: true, get: function () { return __importDefault(xhr_1).default; } });
17
+ var fetch_1 = require("./fetch");
18
+ Object.defineProperty(exports, "onFetch", { enumerable: true, get: function () { return __importDefault(fetch_1).default; } });
@@ -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;