@guomain/monitor-plugins 0.1.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/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @gm Monitor SDK
2
+
3
+ 前端错误监控:运行时错误、Promise、资源加载、接口异常、Vue/React、手动上报、catch 自动采集(需 Vite 插件)。
4
+
5
+ ## 安装
6
+
7
+ ```sh
8
+ pnpm add @guomain/monitor-web
9
+ pnpm add -D @guomain/monitor-plugins @guomain/monitor-types
10
+ ```
11
+
12
+ ## 快速开始
13
+
14
+ ```ts
15
+ import { createWebMonitor } from '@guomain/monitor-web'
16
+
17
+ const monitor = createWebMonitor({
18
+ appId: 'your-app',
19
+ dsn: '/api/monitor',
20
+ vue: { app }
21
+ })
22
+
23
+ globalThis.__GM_MONITOR__ = monitor
24
+ monitor.start()
25
+ ```
26
+
27
+ 完整说明见仓库内 `SDK.md` 与 `PUBLISH.md`。
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './react';
2
+ export * from './vite';
3
+ export * from './vue';
4
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AACtB,cAAc,OAAO,CAAA"}
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './react';
2
+ export * from './vite';
3
+ export * from './vue';
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "license": "MIT",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+http://192.168.200.220/GomainFE/stamp.git",
7
+ "directory": "monitor/packages/monitor-plugins"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "registry": "https://registry.npmjs.org/"
12
+ },
13
+ "name": "@guomain/monitor-plugins",
14
+ "description": "Build-time plugins for @gm monitor SDK (Vite catch injection, Vue/React helpers)",
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "main": "./index.js",
18
+ "module": "./index.js",
19
+ "types": "./index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./index.d.ts",
23
+ "import": "./index.js",
24
+ "default": "./index.js"
25
+ },
26
+ "./vite": {
27
+ "types": "./vite/index.d.ts",
28
+ "import": "./vite/index.js",
29
+ "default": "./vite/index.js"
30
+ },
31
+ "./vue": {
32
+ "types": "./vue/index.d.ts",
33
+ "import": "./vue/index.js",
34
+ "default": "./vue/index.js"
35
+ },
36
+ "./react": {
37
+ "types": "./react/index.d.ts",
38
+ "import": "./react/index.js",
39
+ "default": "./react/index.js"
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "@guomain/monitor-core": "^0.1.0",
44
+ "@guomain/monitor-types": "^0.1.0",
45
+ "@babel/generator": "^7.28.3",
46
+ "@babel/parser": "^7.28.4",
47
+ "@babel/traverse": "^7.28.4",
48
+ "@babel/types": "^7.28.4"
49
+ },
50
+ "peerDependencies": {
51
+ "vite": ">=3.0.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "vite": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "files": [
59
+ "**/*"
60
+ ]
61
+ }
@@ -0,0 +1,9 @@
1
+ import type { MonitorInstance, ReactElementLike, ReactRenderRoot } from '@guomain/monitor-types';
2
+ /**
3
+ * 使用监控 ErrorBoundary 包装 React 渲染入口。
4
+ * @param monitor 当前监控实例。
5
+ * @param root React 渲染根节点。
6
+ * @param element 业务应用节点。
7
+ */
8
+ export declare function renderReactWithMonitor(monitor: MonitorInstance, root: ReactRenderRoot, element: ReactElementLike): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAIhG;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,gBAAgB,QAG1B"}
package/react/index.js ADDED
@@ -0,0 +1,56 @@
1
+ import { toErrorEvent } from '@guomain/monitor-core';
2
+ const boundaryTypes = new WeakMap();
3
+ /**
4
+ * 使用监控 ErrorBoundary 包装 React 渲染入口。
5
+ * @param monitor 当前监控实例。
6
+ * @param root React 渲染根节点。
7
+ * @param element 业务应用节点。
8
+ */
9
+ export function renderReactWithMonitor(monitor, root, element) {
10
+ root.render(createReactBoundaryElement(getReactBoundaryType(monitor), element));
11
+ }
12
+ /**
13
+ * 获取当前监控实例对应的 React ErrorBoundary 类型。
14
+ * @param monitor 当前监控实例。
15
+ */
16
+ function getReactBoundaryType(monitor) {
17
+ const cachedType = boundaryTypes.get(monitor);
18
+ if (cachedType) {
19
+ return cachedType;
20
+ }
21
+ class MonitorReactBoundary {
22
+ static getDerivedStateFromError() {
23
+ return { hasError: true };
24
+ }
25
+ constructor(props) {
26
+ this.state = { hasError: false };
27
+ this.props = props;
28
+ }
29
+ componentDidCatch(error, info) {
30
+ monitor.capture(toErrorEvent('react-error', error, { extra: { info } }));
31
+ }
32
+ render() {
33
+ return this.state.hasError ? null : this.props.children;
34
+ }
35
+ }
36
+ Object.defineProperty(MonitorReactBoundary.prototype, 'isReactComponent', {
37
+ value: {}
38
+ });
39
+ boundaryTypes.set(monitor, MonitorReactBoundary);
40
+ return MonitorReactBoundary;
41
+ }
42
+ /**
43
+ * 创建 React element 形态的边界节点。
44
+ * @param type React ErrorBoundary 类型。
45
+ * @param children 需要被边界包裹的业务节点。
46
+ */
47
+ function createReactBoundaryElement(type, children) {
48
+ return {
49
+ $$typeof: Symbol.for('react.element'),
50
+ type,
51
+ key: null,
52
+ ref: null,
53
+ props: { children },
54
+ _owner: null
55
+ };
56
+ }
@@ -0,0 +1,39 @@
1
+ export interface MonitorCatchPluginOptions {
2
+ /** 是否启用 catch 注入,默认 false。 */
3
+ catch?: boolean;
4
+ /** 需要注入 catch 采集逻辑的文件路径。 */
5
+ include?: string[];
6
+ /** 需要排除的文件路径。 */
7
+ exclude?: string[];
8
+ /** 运行时挂载在 globalThis 上的监控实例变量名。 */
9
+ monitorVar?: string;
10
+ }
11
+ interface VitePluginLike {
12
+ name: string;
13
+ enforce?: 'pre' | 'post';
14
+ transform: (code: string, id: string) => {
15
+ code: string;
16
+ map: null;
17
+ } | null;
18
+ }
19
+ /**
20
+ * 创建 Vite 插件,基于 Babel AST 为 catch 代码注入监控采集。
21
+ * @param options 插件配置,catch 为 true 时才会按 include、exclude 和 monitorVar 注入。
22
+ */
23
+ export declare function monitorCatchPlugin(options: MonitorCatchPluginOptions): VitePluginLike;
24
+ /**
25
+ * 基于 Babel AST 为 catch 语句块注入 monitor.capture()。
26
+ *
27
+ * 注入逻辑:
28
+ * 1. 解析源码为 AST
29
+ * 2. 遍历所有 CatchClause 节点
30
+ * 3. 检查 catch 体内是否已包含上报调用,有则跳过
31
+ * 4. 在 catch 体首行插入 capture(),先于原有代码执行
32
+ *
33
+ * @param code 原始源码。
34
+ * @param monitorVar 运行时监控实例变量名。
35
+ * @param file 当前文件路径,用于上报排查。
36
+ */
37
+ export declare function injectCatchCapture(code: string, monitorVar?: string, file?: string): string;
38
+ export {};
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,yBAAyB;IACxC,8BAA8B;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,iBAAiB;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;CAC5E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,cAAc,CAmCrF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,SAAmB,EAAE,IAAI,SAAK,GAAG,MAAM,CAkEjG"}
package/vite/index.js ADDED
@@ -0,0 +1,248 @@
1
+ import { parse } from '@babel/parser';
2
+ import _traverse from '@babel/traverse';
3
+ import _generate from '@babel/generator';
4
+ import * as t from '@babel/types';
5
+ // @babel/traverse 和 @babel/generator 都是 CJS 包,解构导入
6
+ const traverse = _traverse.default ?? _traverse;
7
+ const generate = _generate.default ?? _generate;
8
+ /**
9
+ * 创建 Vite 插件,基于 Babel AST 为 catch 代码注入监控采集。
10
+ * @param options 插件配置,catch 为 true 时才会按 include、exclude 和 monitorVar 注入。
11
+ */
12
+ export function monitorCatchPlugin(options) {
13
+ const shouldInjectCatch = options.catch === true;
14
+ const monitorVar = options.monitorVar ?? '__GM_MONITOR__';
15
+ assertMonitorVar(monitorVar);
16
+ const include = (options.include ?? []).map(globToRegExp);
17
+ const exclude = (options.exclude ?? []).map(globToRegExp);
18
+ if (shouldInjectCatch && include.length === 0 && typeof console !== 'undefined') {
19
+ console.warn('[gm-monitor-catch] catch 注入已启用但未配置 include,不会注入任何文件');
20
+ }
21
+ return {
22
+ name: 'gm-monitor-catch',
23
+ enforce: 'post',
24
+ transform(code, id) {
25
+ if (!shouldInjectCatch) {
26
+ return null;
27
+ }
28
+ const normalizedId = normalizePath(id);
29
+ if (isSdkInternalPath(normalizedId)) {
30
+ return null;
31
+ }
32
+ if (!include.some(item => item.test(normalizedId))) {
33
+ return null;
34
+ }
35
+ if (exclude.some(item => item.test(normalizedId))) {
36
+ return null;
37
+ }
38
+ const nextCode = injectCatchCapture(code, monitorVar, normalizedId);
39
+ return nextCode === code ? null : { code: nextCode, map: null };
40
+ }
41
+ };
42
+ }
43
+ /**
44
+ * 基于 Babel AST 为 catch 语句块注入 monitor.capture()。
45
+ *
46
+ * 注入逻辑:
47
+ * 1. 解析源码为 AST
48
+ * 2. 遍历所有 CatchClause 节点
49
+ * 3. 检查 catch 体内是否已包含上报调用,有则跳过
50
+ * 4. 在 catch 体首行插入 capture(),先于原有代码执行
51
+ *
52
+ * @param code 原始源码。
53
+ * @param monitorVar 运行时监控实例变量名。
54
+ * @param file 当前文件路径,用于上报排查。
55
+ */
56
+ export function injectCatchCapture(code, monitorVar = '__GM_MONITOR__', file = '') {
57
+ assertMonitorVar(monitorVar);
58
+ let ast;
59
+ try {
60
+ ast = parse(code, {
61
+ sourceType: 'unambiguous',
62
+ plugins: ['typescript', 'jsx', 'decorators-legacy'],
63
+ errorRecovery: true
64
+ });
65
+ }
66
+ catch {
67
+ return code;
68
+ }
69
+ let modified = false;
70
+ const visitor = {
71
+ CatchClause(path) {
72
+ const paramName = path.node.param
73
+ ? t.isIdentifier(path.node.param)
74
+ ? path.node.param.name
75
+ : t.isArrayPattern(path.node.param) || t.isObjectPattern(path.node.param)
76
+ ? '__gmMonitorError'
77
+ : null
78
+ : null;
79
+ const errorRef = paramName ?? '__gmMonitorError';
80
+ // 检查是否已有上报调用
81
+ if (hasMonitorCapture(path, monitorVar)) {
82
+ return;
83
+ }
84
+ // 如果没有参数,添加一个占位参数
85
+ if (!path.node.param) {
86
+ path.node.param = t.identifier('__gmMonitorError');
87
+ }
88
+ // 构造上报表达式
89
+ const captureCall = buildCaptureStatement(monitorVar, errorRef, file);
90
+ const body = path.node.body;
91
+ if (body.body.length > 0) {
92
+ body.body.unshift(captureCall);
93
+ }
94
+ else {
95
+ body.body.push(captureCall);
96
+ }
97
+ modified = true;
98
+ }
99
+ };
100
+ traverse(ast, visitor);
101
+ if (!modified) {
102
+ return code;
103
+ }
104
+ try {
105
+ const result = generate(ast, {
106
+ retainLines: true,
107
+ compact: false
108
+ });
109
+ return result.code;
110
+ }
111
+ catch {
112
+ return code;
113
+ }
114
+ }
115
+ /**
116
+ * 检查 catch 体内是否已包含指定监控变量的 capture 调用。
117
+ * 匹配模式:globalThis.{monitorVar}?.capture?.({ type: 'caught-error', ... })
118
+ */
119
+ function hasMonitorCapture(path, monitorVar) {
120
+ let found = false;
121
+ path.node.body.body.forEach((stmt) => {
122
+ // 检查是否是表达式语句
123
+ if (t.isExpressionStatement(stmt)) {
124
+ if (matchCaptureCall(stmt.expression, monitorVar)) {
125
+ found = true;
126
+ }
127
+ }
128
+ });
129
+ return found;
130
+ }
131
+ /**
132
+ * 递归匹配表达式是否是 capture 调用链。
133
+ * 目标:globalThis.{monitorVar}?.capture?.({ type: 'caught-error', ... })
134
+ */
135
+ function matchCaptureCall(expr, monitorVar) {
136
+ // 最外层:可选调用 (...)
137
+ if (t.isOptionalCallExpression(expr) || t.isCallExpression(expr)) {
138
+ const args = expr.arguments;
139
+ if (args.length === 0)
140
+ return false;
141
+ const firstArg = args[0];
142
+ // 检查是否是对象参数且包含 type: 'caught-error'
143
+ if (t.isObjectExpression(firstArg)) {
144
+ const hasTypeKey = firstArg.properties.some(p => {
145
+ if (t.isObjectProperty(p) && t.isIdentifier(p.key, { name: 'type' })) {
146
+ return t.isStringLiteral(p.value) && p.value.value === 'caught-error';
147
+ }
148
+ return false;
149
+ });
150
+ // 确认调用链
151
+ if (hasTypeKey) {
152
+ return matchMemberChain(expr.callee, monitorVar);
153
+ }
154
+ }
155
+ }
156
+ return false;
157
+ }
158
+ /**
159
+ * 匹配成员访问链:globalThis.{monitorVar}?.capture 或 {monitorVar}.capture
160
+ */
161
+ function matchMemberChain(callee, monitorVar) {
162
+ // capture 属性访问
163
+ if (t.isOptionalMemberExpression(callee) &&
164
+ t.isIdentifier(callee.property, { name: 'capture' })) {
165
+ return matchMonitorVarRef(callee.object, monitorVar);
166
+ }
167
+ // 直接 capture(无可选链)
168
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property, { name: 'capture' })) {
169
+ return matchMonitorVarRef(callee.object, monitorVar);
170
+ }
171
+ return false;
172
+ }
173
+ /**
174
+ * 匹配监控变量引用:globalThis.{monitorVar} 或直接标识符
175
+ */
176
+ function matchMonitorVarRef(expr, monitorVar) {
177
+ // globalThis.{monitorVar}
178
+ if (t.isOptionalMemberExpression(expr) &&
179
+ t.isIdentifier(expr.object, { name: 'globalThis' }) &&
180
+ t.isIdentifier(expr.property, { name: monitorVar })) {
181
+ return true;
182
+ }
183
+ if (t.isMemberExpression(expr) &&
184
+ t.isIdentifier(expr.object, { name: 'globalThis' }) &&
185
+ t.isIdentifier(expr.property, { name: monitorVar })) {
186
+ return true;
187
+ }
188
+ // 直接 {monitorVar}
189
+ if (t.isIdentifier(expr, { name: monitorVar })) {
190
+ return true;
191
+ }
192
+ return false;
193
+ }
194
+ /**
195
+ * 构造上报语句:
196
+ * globalThis.__GM_MONITOR__?.capture?.({ type: 'caught-error', message: ..., stack: ..., timestamp: ..., extra: { file: '...' } })
197
+ */
198
+ function buildCaptureStatement(monitorVar, errorRef, file) {
199
+ const captureFn = t.optionalCallExpression(t.optionalMemberExpression(t.memberExpression(t.identifier('globalThis'), t.identifier(monitorVar)), t.identifier('capture'), false, true), [
200
+ t.objectExpression([
201
+ t.objectProperty(t.identifier('type'), t.stringLiteral('caught-error')),
202
+ t.objectProperty(t.identifier('message'), t.conditionalExpression(t.binaryExpression('instanceof', t.identifier(errorRef), t.identifier('Error')), t.memberExpression(t.identifier(errorRef), t.identifier('message')), t.callExpression(t.identifier('String'), [t.identifier(errorRef)]))),
203
+ t.objectProperty(t.identifier('stack'), t.conditionalExpression(t.binaryExpression('instanceof', t.identifier(errorRef), t.identifier('Error')), t.memberExpression(t.identifier(errorRef), t.identifier('stack')), t.identifier('undefined'))),
204
+ t.objectProperty(t.identifier('timestamp'), t.callExpression(t.memberExpression(t.identifier('Date'), t.identifier('now')), [])),
205
+ t.objectProperty(t.identifier('extra'), t.objectExpression([t.objectProperty(t.identifier('file'), t.stringLiteral(file))]))
206
+ ])
207
+ ], true);
208
+ return t.expressionStatement(captureFn);
209
+ }
210
+ /**
211
+ * 校验监控实例变量名。
212
+ * @param monitorVar 用户配置的全局变量名。
213
+ */
214
+ function assertMonitorVar(monitorVar) {
215
+ if (!/^[A-Za-z_$][\w$]*$/.test(monitorVar)) {
216
+ throw new Error(`Invalid monitorVar: ${monitorVar}`);
217
+ }
218
+ }
219
+ /**
220
+ * 把简单 glob 转成正则。
221
+ * @param glob include/exclude 配置项。
222
+ */
223
+ function globToRegExp(glob) {
224
+ const normalized = normalizePath(glob);
225
+ const placeholder = '__GM_MONITOR_GLOB_STAR__';
226
+ const escaped = normalized
227
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
228
+ .replace(/\*\*/g, placeholder)
229
+ .replace(/\*/g, '[^/]*')
230
+ .replace(/\?/g, '[^/]');
231
+ return new RegExp(escaped.replace(new RegExp(placeholder, 'g'), '.*'));
232
+ }
233
+ /**
234
+ * 统一路径分隔符。
235
+ * @param path 文件路径。
236
+ */
237
+ function normalizePath(path) {
238
+ return path.replace(/\\/g, '/');
239
+ }
240
+ /**
241
+ * 判断是否是 SDK 内部路径
242
+ */
243
+ function isSdkInternalPath(path) {
244
+ return (path.includes('/node_modules/@guomain/monitor-') ||
245
+ path.includes('/node_modules/@gm/monitor-') ||
246
+ path.includes('/monitor/packages/') ||
247
+ /(^|\/)packages\/monitor-(core|web|types|plugins)\//.test(path));
248
+ }
package/vue/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { MonitorInstance, VueMonitorOptions } from '@guomain/monitor-types';
2
+ /**
3
+ * 接入 Vue 全局错误处理。
4
+ * @param options Vue 监控配置,包含 app 实例。
5
+ * @param monitor 当前监控实例。
6
+ */
7
+ export declare function installVueMonitor(options: VueMonitorOptions, monitor: MonitorInstance): () => void;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vue/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAEhF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,eAAe,cAUrF"}
package/vue/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { toErrorEvent } from '@guomain/monitor-core';
2
+ /**
3
+ * 接入 Vue 全局错误处理。
4
+ * @param options Vue 监控配置,包含 app 实例。
5
+ * @param monitor 当前监控实例。
6
+ */
7
+ export function installVueMonitor(options, monitor) {
8
+ const previousHandler = options.app.config.errorHandler;
9
+ options.app.config.errorHandler = (err, instance, info) => {
10
+ monitor.capture(toErrorEvent('vue-error', err, { extra: { info } }));
11
+ previousHandler?.(err, instance, info);
12
+ };
13
+ return () => {
14
+ options.app.config.errorHandler = previousHandler;
15
+ };
16
+ }