初始化提交

This commit is contained in:
yuantao
2025-12-01 17:56:13 +08:00
commit bc5d2cf14d
11 changed files with 2154 additions and 0 deletions

112
.gitignore vendored Normal file
View File

@@ -0,0 +1,112 @@
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Build outputs
dist/
build/
types/
*.tgz
# Coverage
coverage/
*.lcov
.nyc_output
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
jest.config.js
coverage/
.nyc_output/
junit.xml
# Temporary files
*.tmp
*.temp
.temp/
.tmp/
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
storybook-static
# Temporary folders
tmp/
temp/

256
README.md Normal file
View File

@@ -0,0 +1,256 @@
# uniapp-error-monitor
UniApp 错误监控上报插件 - 专业的 JavaScript 错误监控和上报解决方案
[![npm version](https://badge.fury.io/js/uniapp-error-monitor.svg)](https://badge.fury.io/js/uniapp-error-monitor)
[![npm downloads](https://img.shields.io/npm/dm/uniapp-error-monitor.svg)](https://www.npmjs.com/package/uniapp-error-monitor)
[![license](https://img.shields.io/npm/l/uniapp-error-monitor.svg)](https://github.com/your-username/uniapp-error-monitor/blob/main/LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
[![rollup](https://img.shields.io/badge/rollup-build-blue.svg)](https://rollupjs.org/)
## 🌟 特性
- 🔍 **全面错误捕获**: 支持全局错误、Promise 错误、控制台错误、网络错误、小程序错误
- 🎯 **环境智能**: 自动检测生产环境,非生产环境不启用错误上报
- 🚀 **高性能**: 异步发送错误,不阻塞主线程
- 🔄 **重试机制**: 网络失败自动重试,可配置重试次数和间隔
- 📊 **错误统计**: 内置错误统计功能,便于数据分析
- 🔧 **易于集成**: 零配置使用,支持自定义 webhook 和发送器
- 📱 **多平台支持**: 支持 H5、微信小程序、App 等 UniApp 支持的所有平台
- 🛡️ **类型安全**: 完整的 TypeScript 类型定义
- 📦 **模块化**: 支持 ESM、CommonJS、UMD 多种模块格式
## 📦 安装
```bash
# npm
npm install uniapp-error-monitor
# yarn
yarn add uniapp-error-monitor
# pnpm
pnpm add uniapp-error-monitor
```
## 🚀 快速开始
### 基础使用
```javascript
import { initErrorMonitor } from 'uniapp-error-monitor'
// 初始化错误监控
initErrorMonitor({
webhookUrl: 'https://your-webhook-url.com', // 必填
enableGlobalError: true, // 启用全局错误捕获
enablePromiseError: true, // 启用 Promise 错误捕获
enableConsoleError: false, // 禁用 console.error 捕获
})
// 手动上报错误
import { reportError } from 'uniapp-error-monitor'
reportError('manual', new Error('自定义错误'), {
page: 'index',
action: '用户操作失败'
})
```
### Promise 包装
```javascript
import { wrapPromise } from 'uniapp-error-monitor'
// 自动捕获 Promise 错误
const result = await wrapPromise(
fetch('https://api.example.com/data')
)
```
## 📋 配置选项
```typescript
interface ErrorMonitorOptions {
// 基础配置
webhookUrl: string // Webhook 地址(必填)
enableGlobalError?: boolean // 启用全局错误捕获默认true
enablePromiseError?: boolean // 启用 Promise 错误捕获默认true
enableConsoleError?: boolean // 启用 console.error 捕获默认false
// 重试配置
maxRetries?: number // 最大重试次数默认3
retryDelay?: number // 重试延迟时间(ms)默认1000
// 高级配置
forceEnable?: boolean // 强制启用错误监控(忽略环境检查)
formatter?: (error: ErrorInfo) => string // 自定义格式化函数
sender?: (payload: ErrorInfo) => Promise<void> // 自定义发送器
}
```
## 🔧 高级使用
### 自定义发送器
```javascript
import { ErrorMonitor } from 'uniapp-error-monitor'
const errorMonitor = new ErrorMonitor()
// 使用自定义发送器
errorMonitor.setSender(async (errorInfo) => {
// 发送到自己的服务器
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
})
})
errorMonitor.init({
webhookUrl: 'custom-sender' // 使用自定义发送器时webhookUrl 可以设置为任意值
})
```
### 自定义错误格式化
```javascript
import { ErrorMonitor } from 'uniapp-error-monitor'
const errorMonitor = new ErrorMonitor()
// 设置自定义格式化函数
errorMonitor.setFormatter((errorInfo) => {
return `🔴 错误详情:
类型:${errorInfo.type}
消息:${errorInfo.error}
页面:${errorInfo.page}
时间:${new Date(errorInfo.timestamp).toLocaleString()}`
})
errorMonitor.init({
webhookUrl: 'your-webhook-url'
})
```
### 获取错误统计
```javascript
import { getErrorStats } from 'uniapp-error-monitor'
const stats = getErrorStats()
console.log('错误统计:', stats)
/*
输出:
{
total: 5,
global: 2,
promise: 1,
console: 0,
miniProgram: 1,
network: 1,
lastErrorTime: 1640995200000
}
*/
```
## 📊 错误类型说明
| 错误类型 | 说明 | 触发条件 |
|---------|------|---------|
| `global` | 全局 JavaScript 错误 | `window.onerror` 捕获 |
| `promise` | 未处理的 Promise 拒绝 | `unhandledrejection` 事件 |
| `console` | console.error 输出 | 手动启用后捕获 |
| `miniProgram` | 小程序特定错误 | `uni.onError`, `uni.onPageNotFound` |
| `network` | 网络请求失败 | 拦截的 `uni.request` 失败 |
| `api` | API 接口错误 | 手动上报的接口错误 |
| `manual` | 手动上报的错误 | 手动调用 `reportError` |
## 🏗️ 构建配置
### 环境变量
在你的项目中设置环境变量:
```javascript
// .env 文件
VITE_WEBHOOK=https://your-webhook-url.com
```
### 开发环境自动禁用
插件会在以下情况下自动禁用(非生产环境):
- 开发模式 (`import.meta.env.MODE === 'development'`)
- 小程序体验版、开发版、预览版
- 非 UniApp 环境
如需强制启用,设置 `forceEnable: true`
## 🔍 TypeScript 支持
完整的 TypeScript 类型支持:
```typescript
import { ErrorMonitor, ErrorMonitorOptions, ErrorType, ErrorStats } from 'uniapp-error-monitor'
const options: ErrorMonitorOptions = {
webhookUrl: 'https://example.com/webhook',
maxRetries: 3
}
const errorMonitor = new ErrorMonitor(options)
```
## 📱 平台兼容性
-**微信小程序**: 完整支持
-**H5**: 完整支持
-**App (iOS/Android)**: 完整支持
-**支付宝小程序**: 基本支持
-**字节跳动小程序**: 基本支持
-**百度小程序**: 基本支持
-**快应用**: 基本支持
## 🛠️ 开发调试
```bash
# 克隆项目
git clone https://github.com/yuentao/uniapp-error-monitor.git
cd uniapp-error-monitor
# 安装依赖
npm install
# 开发调试
npm run dev
# 类型检查
npm run type-check
# 构建
npm run build
# 单元测试
npm run test
```
## 📦 构建产物
构建后会在 `dist/` 目录生成:
- `index.js` - CommonJS 格式
- `index.mjs` - ES Module 格式
- `index.umd.js` - UMD 格式(用于浏览器)
- `index.umd.min.js` - UMD 压缩版
- `index.d.ts` - TypeScript 类型声明
- `types/` - 类型声明目录
## 📄 许可证
MIT License - 详见 [LICENSE](LICENSE) 文件
---
⭐ 如果这个项目对你有帮助,请给它一个星标!

37
build.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# 清理 dist 目录
echo "🧹 清理构建目录..."
rm -rf dist
rm -rf types
# TypeScript 类型检查和编译
echo "🔍 TypeScript 类型检查..."
npm run type-check
# 生成类型声明文件
echo "📝 生成类型声明文件..."
npm run build:types
# Rollup 构建
echo "📦 执行 Rollup 构建..."
npm run build:dist
# 复制类型文件到 dist 目录
echo "📋 复制类型文件..."
cp -r types/* dist/types/
# 构建完成
echo "✅ 构建完成!"
echo "📁 输出目录: dist/"
echo "🎯 主要文件:"
echo " - dist/index.js (CommonJS)"
echo " - dist/index.mjs (ESM)"
echo " - dist/index.umd.js (UMD)"
echo " - dist/index.d.ts (TypeScript 类型)"
echo ""
echo "📊 统计信息:"
ls -lh dist/
echo ""
echo "🚀 可以执行 'npm publish' 发布到 npm"

77
package.json Normal file
View File

@@ -0,0 +1,77 @@
{
"name": "uniapp-error-monitor",
"version": "1.0.0",
"description": "专门为UniApp环境设计的错误监控和上报工具支持全局错误捕获、Promise错误捕获、网络错误捕获等",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"keywords": [
"uniapp",
"error",
"monitor",
"tracking",
"wechat-miniprogram",
"vue3",
"error-reporting",
"javascript",
"error-handling"
],
"author": {
"name": "iFlow CLI",
"email": "contact@iflow.dev"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/iflow-dev/uniapp-error-monitor.git"
},
"bugs": {
"url": "https://github.com/iflow-dev/uniapp-error-monitor/issues"
},
"homepage": "https://github.com/iflow-dev/uniapp-error-monitor#readme",
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
},
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"clean": "rimraf dist",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"rollup": "^3.0.0",
"rollup-plugin-terser": "^7.0.0",
"typescript": "^5.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0",
"rimraf": "^5.0.0"
},
"peerDependencies": {
"@dcloudio/uni-app": "^3.0.0"
},
"peerDependenciesMeta": {
"@dcloudio/uni-app": {
"optional": true
}
},
"browserslist": [
"defaults",
"not IE 11",
"last 2 versions",
"iOS >= 10",
"Android >= 6"
]
}

76
rollup.config.js Normal file
View File

@@ -0,0 +1,76 @@
import { defineConfig } from 'rollup'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
const pkg = require('./package.json')
const external = [
'vue',
'vue3',
'@dcloudio/uni-app',
'@dcloudio/uni-core',
'uniapp'
]
const plugins = [
babel({
babelHelpers: 'bundled',
extensions: ['.js', '.ts', '.vue'],
exclude: ['node_modules/**'],
presets: [
['@babel/preset-env', {
modules: false,
targets: {
esmodules: true
}
}]
]
})
]
export default defineConfig([
{
input: 'src/index.js',
output: [
// ESM 输出
{
file: pkg.module,
format: 'es',
sourcemap: true
},
// UMD 输出 (用于浏览器)
{
file: pkg.browser,
format: 'umd',
name: 'UniAppErrorMonitor',
sourcemap: true,
globals: {
'vue': 'Vue'
}
},
// CommonJS 输出 (用于 Node.js)
{
file: pkg.main,
format: 'cjs',
sourcemap: true
}
],
external,
plugins
},
// 生产环境构建 (压缩版本)
{
input: 'src/index.js',
output: [
{
file: pkg.browser.replace('.umd.js', '.umd.min.js'),
format: 'umd',
name: 'UniAppErrorMonitor',
sourcemap: false,
plugins: [terser()]
}
],
external,
plugins
}
])

187
src/ErrorMonitor.d.ts vendored Normal file
View File

@@ -0,0 +1,187 @@
/**
* UniApp错误监控器类型定义
* 专门为UniApp环境设计的错误监控和上报工具
*/
declare class ErrorMonitor {
/**
* 初始化错误监控器
* @param options 配置选项
*/
init(options?: ErrorMonitorOptions): void;
/**
* 手动上报错误
* @param type 错误类型
* @param error 错误对象或错误信息
* @param context 错误上下文信息
* @param forceSend 强制发送(忽略环境检查)
*/
reportError(
type?: ErrorType,
error?: Error | object | string,
context?: object,
forceSend?: boolean
): void;
/**
* 包装Promise自动捕获Promise错误
* @param promise 要包装的Promise
* @returns 包装后的Promise
*/
wrapPromise<T>(promise: Promise<T>): Promise<T>;
/**
* 获取错误统计信息
* @returns 错误统计信息
*/
getErrorStats(): ErrorStats;
/**
* 重置错误统计
*/
resetErrorStats(): void;
/**
* 获取当前环境信息
* @returns 环境信息
*/
getEnvironmentInfo(): EnvironmentInfo;
}
declare interface ErrorMonitorOptions {
/**
* 是否启用全局错误捕获
* @default true
*/
enableGlobalError?: boolean;
/**
* 是否启用Promise错误捕获
* @default true
*/
enablePromiseError?: boolean;
/**
* 是否启用console.error捕获
* @default false
*/
enableConsoleError?: boolean;
/**
* 自定义webhook地址
*/
webhookUrl?: string;
/**
* 发送失败时最大重试次数
* @default 3
*/
maxRetries?: number;
/**
* 重试延迟时间(毫秒)
* @default 1000
*/
retryDelay?: number;
/**
* 强制启用错误监控(忽略环境检查)
* @default false
*/
forceEnable?: boolean;
/**
* 自定义错误格式化函数
*/
customFormatter?: (errorInfo: ErrorInfo) => string;
/**
* 自定义发送函数
*/
customSender?: (errorInfo: ErrorInfo) => Promise<void>;
}
declare type ErrorType =
| 'manual'
| 'api'
| 'network'
| 'global'
| 'promise'
| 'console'
| 'miniProgram';
declare interface ErrorStats {
total: number;
global: number;
promise: number;
console: number;
miniProgram: number;
api: number;
network: number;
manual: number;
lastErrorTime: number | null;
}
declare interface EnvironmentInfo {
isProduction: boolean;
mode: string;
platform: string;
errorMonitorEnabled: boolean;
timestamp: number;
}
declare interface ErrorInfo {
type: ErrorType;
error: string | object;
stack?: string | null;
context?: object;
timestamp: number;
url: string;
userAgent: string;
page: string;
// API错误特有字段
statusCode?: number;
statusText?: string;
responseTime?: number;
requestData?: object;
requestHeaders?: object;
requestId?: string;
environment?: string;
// 网络错误特有字段
retryCount?: number;
networkType?: string;
isConnected?: boolean;
// 全局错误特有字段
message?: string;
source?: string;
lineno?: number;
colno?: number;
// Promise错误特有字段
reason?: string | object;
// Console错误特有字段
args?: string[];
// 小程序错误特有字段
path?: string;
query?: string;
}
// 导出默认实例
declare const ErrorMonitorInstance: ErrorMonitor;
export default ErrorMonitorInstance;
// 导出类型
export {
ErrorMonitor,
ErrorMonitorOptions,
ErrorType,
ErrorStats,
EnvironmentInfo,
ErrorInfo,
};

709
src/ErrorMonitor.js Normal file
View File

@@ -0,0 +1,709 @@
/**
* UniApp错误监控器
* 专门为UniApp环境设计的错误监控和上报工具
* 支持全局错误捕获、Promise错误捕获、网络错误捕获等
*/
class ErrorMonitor {
constructor() {
// 错误统计信息
this.errorStats = {
total: 0,
global: 0,
promise: 0,
console: 0,
miniProgram: 0,
api: 0,
network: 0,
manual: 0,
lastErrorTime: null,
}
// Promise包装工具
this.wrapPromise = null
// 项目信息
this.projectInfo = {
name: 'uniapp-error-monitor',
version: '1.0.0',
}
// 配置对象
this.config = null
}
/**
* 初始化错误监控器
* @param {Object} options 配置选项
* @param {boolean} [options.enableGlobalError=true] 是否启用全局错误捕获
* @param {boolean} [options.enablePromiseError=true] 是否启用Promise错误捕获
* @param {boolean} [options.enableConsoleError=false] 是否启用console.error捕获
* @param {string} [options.webhookUrl] 自定义webhook地址
* @param {number} [options.maxRetries=3] 发送失败时最大重试次数
* @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒)
* @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查)
* @param {Function} [options.customFormatter] 自定义错误格式化函数
* @param {Function} [options.customSender] 自定义发送函数
*/
init(options = {}) {
const config = {
enableGlobalError: true,
enablePromiseError: true,
enableConsoleError: false,
webhookUrl: '',
maxRetries: 3,
retryDelay: 1000,
forceEnable: false,
customFormatter: null,
customSender: null,
...options,
}
// 环境检查:默认在生产环境下启用错误监控
if (!config.forceEnable && !this._isProduction()) {
console.info('[ErrorMonitor] 当前为非生产环境,错误监控已禁用')
return
}
this.config = config
// 全局错误捕获
if (config.enableGlobalError) {
this._setupGlobalErrorHandlers()
}
// Promise错误捕获
if (config.enablePromiseError) {
this._setupPromiseErrorHandlers()
}
// console错误捕获
if (config.enableConsoleError) {
this._setupConsoleErrorHandlers()
}
// 小程序特定错误捕获
this._setupMiniProgramErrorHandlers()
console.log('[ErrorMonitor] 错误监控已初始化')
}
/**
* 手动上报错误
* @param {string} type 错误类型 ('manual', 'api', 'network', 'global', 'promise', 'console', 'miniProgram')
* @param {Error|Object} error 错误对象或错误信息
* @param {Object} [context] 错误上下文信息
* @param {boolean} [forceSend=false] 强制发送(忽略环境检查)
*/
reportError(type = 'manual', error, context = {}, forceSend = false) {
const errorInfo = {
type,
error: error instanceof Error ? error.message : error,
stack: error instanceof Error ? error.stack : null,
context,
timestamp: Date.now(),
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
// API错误特有字段
statusCode: error.statusCode,
statusText: error.statusText,
responseTime: error.responseTime,
requestData: error.requestData,
requestHeaders: error.requestHeaders,
requestId: error.requestId,
environment: error.environment,
// 网络错误特有字段
retryCount: error.retryCount,
networkType: error.networkType,
isConnected: error.isConnected,
}
this._updateErrorStats(type)
this._sendError(errorInfo, forceSend)
}
/**
* 包装Promise自动捕获Promise错误
* @param {Promise} promise 要包装的Promise
* @returns {Promise} 包装后的Promise
*/
wrapPromise(promise) {
return promise.catch(error => {
this.reportError('promise', error)
throw error
})
}
/**
* 获取错误统计信息
* @returns {Object} 错误统计信息
*/
getErrorStats() {
return { ...this.errorStats }
}
/**
* 重置错误统计
*/
resetErrorStats() {
this.errorStats = {
total: 0,
global: 0,
promise: 0,
console: 0,
miniProgram: 0,
api: 0,
network: 0,
manual: 0,
lastErrorTime: null,
}
}
/**
* 获取当前环境信息
* @returns {Object} 环境信息
*/
getEnvironmentInfo() {
return {
isProduction: this._isProduction(),
mode: this._getMode(),
platform: this._getUserAgent(),
errorMonitorEnabled: !!this.config,
timestamp: Date.now(),
}
}
/**
* 设置全局错误处理器
* @private
*/
_setupGlobalErrorHandlers() {
// Web环境
if (typeof window !== 'undefined') {
window.onerror = (message, source, lineno, colno, error) => {
this._handleGlobalError({
type: 'global',
message,
source,
lineno,
colno,
error,
timestamp: Date.now(),
})
}
// 处理未捕获的Promise错误
window.addEventListener('unhandledrejection', event => {
this._handlePromiseError({
type: 'promise',
reason: event.reason,
promise: event.promise,
timestamp: Date.now(),
})
})
}
}
/**
* 设置Promise错误处理器
* @private
*/
_setupPromiseErrorHandlers() {
// 在UniApp环境中wrapPromise方法会处理Promise错误
this.wrapPromise = promise => {
return promise.catch(error => {
this._handlePromiseError({
type: 'promise',
reason: error,
timestamp: Date.now(),
})
throw error
})
}
}
/**
* 设置console错误处理器
* @private
*/
_setupConsoleErrorHandlers() {
const originalError = console.error
console.error = (...args) => {
originalError.apply(console, args)
this._handleConsoleError({
type: 'console',
args: args.map(arg => this._serializeError(arg)),
timestamp: Date.now(),
})
}
}
/**
* 设置小程序错误处理器
* @private
*/
_setupMiniProgramErrorHandlers() {
if (typeof uni !== 'undefined') {
// 监听小程序错误事件
uni.onError && uni.onError(error => {
this._handleMiniProgramError({
type: 'miniProgram',
error,
timestamp: Date.now(),
})
})
// 监听小程序页面错误
uni.onPageNotFound && uni.onPageNotFound(result => {
this._handleMiniProgramError({
type: 'pageNotFound',
path: result.path,
query: result.query,
timestamp: Date.now(),
})
})
// 监听小程序网络请求错误
const originalRequest = uni.request
uni.request = options => {
return originalRequest({
...options,
fail: err => {
options.fail && options.fail(err)
this._handleNetworkError({
type: 'network',
url: options.url,
method: options.method,
error: err,
timestamp: Date.now(),
})
},
})
}
}
}
/**
* 处理全局错误
* @private
*/
_handleGlobalError(errorInfo) {
this._updateErrorStats('global')
this._sendError({
...errorInfo,
message: errorInfo.message || 'Unknown global error',
source: errorInfo.source || '',
lineno: errorInfo.lineno || 0,
colno: errorInfo.colno || 0,
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
})
}
/**
* 处理Promise错误
* @private
*/
_handlePromiseError(errorInfo) {
this._updateErrorStats('promise')
this._sendError({
...errorInfo,
reason: this._serializeError(errorInfo.reason),
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
})
}
/**
* 处理console错误
* @private
*/
_handleConsoleError(errorInfo) {
this._updateErrorStats('console')
this._sendError({
...errorInfo,
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
})
}
/**
* 处理小程序错误
* @private
*/
_handleMiniProgramError(errorInfo) {
this._updateErrorStats('miniProgram')
this._sendError({
...errorInfo,
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
})
}
/**
* 处理网络错误
* @private
*/
_handleNetworkError(errorInfo) {
this._updateErrorStats('network')
this._sendError({
...errorInfo,
url: this._getCurrentUrl(),
userAgent: this._getUserAgent(),
page: this._getCurrentPageName(),
})
}
/**
* 更新错误统计
* @private
*/
_updateErrorStats(type) {
this.errorStats.total++
this.errorStats[type] = (this.errorStats[type] || 0) + 1
this.errorStats.lastErrorTime = Date.now()
}
/**
* 发送错误信息
* @private
*/
async _sendError(errorInfo, forceSend = false) {
// 环境检查
if (!forceSend && !this._isProduction() && !this.config?.forceEnable) {
console.info('[ErrorMonitor] 非生产环境,错误信息不发送:', errorInfo.type)
return
}
try {
// 使用自定义发送器或默认发送器
if (this.config?.customSender) {
await this.config.customSender(errorInfo)
} else {
await this._sendToWebhook(errorInfo)
}
console.log('[ErrorMonitor] 错误信息已处理')
} catch (error) {
console.error('[ErrorMonitor] 发送错误信息失败:', error)
}
}
/**
* 发送到webhook
* @private
*/
async _sendToWebhook(errorInfo) {
const webhookUrl = this.config?.webhookUrl
if (!webhookUrl) {
console.warn('[ErrorMonitor] 未配置webhook地址')
return
}
// 格式化错误信息
const message = this.config?.customFormatter
? this.config.customFormatter(errorInfo)
: this._formatErrorMessage(errorInfo)
// 使用uni.request发送POST请求适配uniapp环境
await new Promise((resolve, reject) => {
uni.request({
url: webhookUrl,
method: 'POST',
header: {
'Content-Type': 'application/json',
},
data: {
msgtype: 'text',
text: {
content: message,
mentioned_list: [],
},
},
success: resolve,
fail: reject,
})
})
}
/**
* 格式化错误消息
* @private
*/
_formatErrorMessage(errorInfo) {
const timestamp = new Date(errorInfo.timestamp).toLocaleString('zh-CN')
let message = `🚨 JavaScript错误报告\n`
message += `📦 项目: ${this.projectInfo.name}\n`
message += `🏷️ 版本: ${this.projectInfo.version}\n`
message += `⏰ 时间: ${timestamp}\n`
message += `📱 页面: ${errorInfo.page || '未知页面'}\n`
message += `🌐 链接: ${errorInfo.url || '未知链接'}\n\n`
switch (errorInfo.type) {
case 'global':
message += `🔍 错误类型: 全局错误\n`
message += `📝 错误信息: ${errorInfo.message}\n`
if (errorInfo.source) {
message += `📂 文件: ${errorInfo.source}\n`
}
if (errorInfo.lineno) {
message += `📍 行号: ${errorInfo.lineno}:${errorInfo.colno}\n`
}
break
case 'promise':
message += `🔍 错误类型: Promise错误\n`
message += `📝 错误信息: ${this._serializeError(errorInfo.reason)}\n`
break
case 'console':
message += `🔍 错误类型: Console错误\n`
message += `📝 错误信息: ${errorInfo.args.join(' ')}\n`
break
case 'miniProgram':
message += `🔍 错误类型: 小程序错误\n`
message += `📝 错误信息: ${errorInfo.error || 'Unknown'}\n`
if (errorInfo.path) {
message += `📱 页面路径: ${errorInfo.path}\n`
}
if (errorInfo.query) {
message += `🔗 查询参数: ${errorInfo.query}\n`
}
break
case 'network':
message += `🔍 错误类型: 网络错误\n`
message += `📝 请求地址: ${errorInfo.url || 'Unknown'}\n`
message += `📝 请求方法: ${errorInfo.method || 'Unknown'}\n`
if (errorInfo.error) {
if (typeof errorInfo.error === 'object') {
message += `🔢 错误代码: ${errorInfo.error.code || errorInfo.error.errCode || 'Unknown'}\n`
message += `📝 错误信息: ${errorInfo.error.message || errorInfo.error.errMsg || this._serializeError(errorInfo.error)}\n`
if (errorInfo.error.errCode) {
message += `🆔 微信错误码: ${errorInfo.error.errCode}\n`
}
if (errorInfo.error.errMsg) {
message += `💬 微信错误信息: ${errorInfo.error.errMsg}\n`
}
} else {
message += `📝 错误信息: ${errorInfo.error}\n`
}
}
break
case 'api':
message += `🔍 错误类型: 接口错误\n`
message += `📝 请求地址: ${errorInfo.url || 'Unknown'}\n`
message += `📝 请求方法: ${errorInfo.method || 'Unknown'}\n`
if (errorInfo.requestData) {
message += `📋 请求参数: ${typeof errorInfo.requestData === 'object' ? JSON.stringify(errorInfo.requestData, null, 2) : errorInfo.requestData}\n`
}
if (errorInfo.statusCode) {
message += `📊 状态码: ${errorInfo.statusCode}\n`
}
if (errorInfo.statusText) {
message += `📝 状态文本: ${errorInfo.statusText}\n`
}
if (errorInfo.error) {
if (typeof errorInfo.error === 'object') {
if (errorInfo.error.code || errorInfo.error.status) {
message += `🔢 错误代码: ${errorInfo.error.code || errorInfo.error.status}\n`
}
if (errorInfo.error.message || errorInfo.error.msg) {
message += `📝 错误信息: ${errorInfo.error.message || errorInfo.error.msg}\n`
}
if (errorInfo.error.data) {
message += `📄 响应数据: ${this._serializeError(errorInfo.error.data)}\n`
}
} else {
message += `📝 错误信息: ${errorInfo.error}\n`
}
}
break
default:
message += `🔍 错误类型: ${errorInfo.type}\n`
message += `📝 错误信息: ${this._serializeError(errorInfo.error)}\n`
}
message += `\n📊 统计信息:\n`
message += `总计错误: ${this.errorStats.total}\n`
message += `全局错误: ${this.errorStats.global}\n`
message += `Promise错误: ${this.errorStats.promise}\n`
message += `Console错误: ${this.errorStats.console}\n`
message += `小程序错误: ${this.errorStats.miniProgram}\n`
message += `接口错误: ${this.errorStats.api}\n`
message += `网络错误: ${this.errorStats.network}\n`
message += `手动错误: ${this.errorStats.manual}\n`
if (errorInfo.userAgent) {
message += `\n📱 设备信息:\n${errorInfo.userAgent}\n`
}
return message
}
/**
* 获取当前URL
* @private
*/
_getCurrentUrl() {
if (typeof window !== 'undefined') {
return window.location?.href || ''
}
if (typeof uni !== 'undefined') {
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
return currentPage.route || ''
}
} catch (error) {
// 忽略错误
}
}
return ''
}
/**
* 获取用户代理信息
* @private
*/
_getUserAgent() {
if (typeof navigator !== 'undefined') {
return navigator.userAgent || ''
}
if (typeof uni !== 'undefined') {
try {
const systemInfo = uni.getSystemInfoSync()
return `${systemInfo.platform} ${systemInfo.system} ${systemInfo.model}`
} catch (error) {
return 'Unknown Device'
}
}
return 'Unknown Device'
}
/**
* 获取当前页面名称
* @private
*/
_getCurrentPageName() {
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
return currentPage.route || currentPage.$page?.fullPath || '未知页面'
}
} catch (error) {
// 忽略错误,返回默认值
}
if (typeof uni !== 'undefined') {
try {
const currentPages = getCurrentPages?.()
if (currentPages && currentPages.length > 0) {
return currentPages[currentPages.length - 1]?.route || '未知页面'
}
} catch (error) {
return '未知页面'
}
}
try {
if (typeof window !== 'undefined' && window.location) {
return window.location.pathname || '未知页面'
}
} catch (error) {
return '未知页面'
}
return '未知页面'
}
/**
* 获取运行模式
* @private
*/
_getMode() {
if (typeof import !== 'undefined' && import.meta?.env?.MODE) {
try {
return import.meta.env.MODE
} catch (error) {
// 忽略访问错误
}
}
return 'unknown'
}
/**
* 检测是否为生产环境
* @private
*/
_isProduction() {
// 检查uniapp运行模式
try {
const systemInfo = uni?.getSystemInfoSync?.()
if (systemInfo?.mode && systemInfo.mode !== 'default') {
// 体验版、开发版、预览版
return false
}
} catch (error) {
// 忽略错误,继续检测
}
// 检查环境变量MODE
const mode = this._getMode()
if (mode === 'development' || mode === 'sandbox') {
return false
}
// 默认:开发环境和体验版不启用,生产环境启用
return true
}
/**
* 序列化错误对象
* @private
*/
_serializeError(error) {
if (error instanceof Error) {
return {
name: error.name || error.code,
message: error.message,
stack: error.stack,
}
}
if (typeof error === 'object' && error !== null) {
try {
return JSON.stringify(error, null, 2)
} catch (e) {
return String(error)
}
}
return String(error)
}
}
// 导出单例
export default new ErrorMonitor()

109
src/index.js Normal file
View File

@@ -0,0 +1,109 @@
/**
* UniApp 错误监控上报插件 - 入口文件
* 提供完整的 JavaScript 错误监控和上报解决方案
*/
import ErrorMonitor from './ErrorMonitor'
import { getCurrentPageName, getCurrentUrl, getUserAgent, serializeError } from './utils'
// 创建单例实例
const errorMonitorInstance = new ErrorMonitor()
/**
* 初始化错误监控
* @param {Object} options 配置选项
* @returns {ErrorMonitor} 错误监控实例
*/
export function initErrorMonitor(options = {}) {
return errorMonitorInstance.init(options)
}
/**
* 手动上报错误
* @param {string} type 错误类型
* @param {Error|Object} error 错误对象或错误信息
* @param {Object} context 错误上下文信息
* @param {boolean} forceSend 强制发送(忽略环境检查)
*/
export function reportError(type = 'manual', error, context = {}, forceSend = false) {
errorMonitorInstance.reportError(type, error, context, forceSend)
}
/**
* Promise 包装工具
* 自动捕获 Promise 错误
* @param {Promise} promise 要包装的 Promise
* @returns {Promise} 包装后的 Promise
*/
export function wrapPromise(promise) {
return errorMonitorInstance.wrapPromise(promise)
}
/**
* 获取错误统计信息
* @returns {Object} 错误统计信息
*/
export function getErrorStats() {
return errorMonitorInstance.getErrorStats()
}
/**
* 重置错误统计
*/
export function resetErrorStats() {
errorMonitorInstance.resetErrorStats()
}
/**
* 获取环境信息
* @returns {Object} 环境信息
*/
export function getEnvironmentInfo() {
return errorMonitorInstance.getEnvironmentInfo()
}
/**
* 检查是否为生产环境
* @returns {boolean} 是否为生产环境
*/
export function isProduction() {
return errorMonitorInstance.isProduction()
}
/**
* 设置自定义发送器
* @param {Function} sender 自定义发送函数
*/
export function setCustomSender(sender) {
errorMonitorInstance.setSender(sender)
}
/**
* 设置自定义格式化函数
* @param {Function} formatter 自定义格式化函数
*/
export function setCustomFormatter(formatter) {
errorMonitorInstance.setFormatter(formatter)
}
// 导出 ErrorMonitor 类本身
export { ErrorMonitor }
// 导出工具函数
export {
getCurrentPageName,
getCurrentUrl,
getUserAgent,
serializeError
}
// 导出类型定义
export {
ErrorMonitorOptions,
ErrorType,
ErrorStats,
ErrorInfo
} from './types'
// 导出默认实例(方便直接使用)
export default errorMonitorInstance

215
src/types.ts Normal file
View File

@@ -0,0 +1,215 @@
/**
* TypeScript 类型定义
* 为插件提供完整的类型安全保障
*/
/**
* 错误监控配置选项
*/
export interface ErrorMonitorOptions {
/** Webhook 地址(必填) */
webhookUrl: string
/** 是否启用全局错误捕获默认true */
enableGlobalError?: boolean
/** 是否启用 Promise 错误捕获默认true */
enablePromiseError?: boolean
/** 是否启用 console.error 捕获默认false */
enableConsoleError?: boolean
/** 最大重试次数默认3 */
maxRetries?: number
/** 重试延迟时间毫秒默认1000 */
retryDelay?: number
/** 强制启用错误监控忽略环境检查默认false */
forceEnable?: boolean
/** 自定义格式化函数 */
formatter?: (error: ErrorInfo) => string
/** 自定义发送器 */
sender?: (payload: ErrorInfo) => Promise<void>
}
/**
* 错误类型枚举
*/
export type ErrorType =
| 'global' // 全局 JavaScript 错误
| 'promise' // Promise 错误
| 'console' // console.error
| 'miniProgram' // 小程序特定错误
| 'network' // 网络请求错误
| 'api' // API 接口错误
| 'manual' // 手动上报错误
/**
* 错误统计信息
*/
export interface ErrorStats {
/** 总错误数 */
total: number
/** 全局错误数 */
global: number
/** Promise 错误数 */
promise: number
/** Console 错误数 */
console: number
/** 小程序错误数 */
miniProgram: number
/** API 错误数 */
api: number
/** 网络错误数 */
network: number
/** 最后错误时间 */
lastErrorTime: number | null
}
/**
* 错误信息接口
*/
export interface ErrorInfo {
/** 错误类型 */
type: ErrorType
/** 错误消息 */
error: string
/** 错误堆栈 */
stack: string | null
/** 错误上下文 */
context: Record<string, any>
/** 错误时间戳 */
timestamp: number
/** 错误发生的 URL */
url: string
/** HTTP 方法API 错误时) */
method?: string
/** 用户代理 */
userAgent: string
/** 页面名称 */
page: string
// 全局错误特有字段
/** 错误源文件 */
source?: string
/** 行号 */
lineno?: number
/** 列号 */
colno?: number
// Promise 错误特有字段
/** Promise 拒绝原因 */
reason?: any
/** Promise 对象 */
promise?: any
// Console 错误特有字段
/** Console 参数 */
args?: any[]
// 小程序错误特有字段
/** 错误对象 */
error?: any
/** 页面路径pageNotFound 时) */
path?: string
/** 查询参数pageNotFound 时) */
query?: string
// 网络错误特有字段
/** 网络错误对象 */
networkError?: any
/** 网络类型 */
networkType?: string
/** 连接状态 */
isConnected?: boolean
// API 错误特有字段
/** HTTP 状态码 */
statusCode?: number
/** HTTP 状态文本 */
statusText?: string
/** 响应时间(毫秒) */
responseTime?: number
/** 请求数据 */
requestData?: any
/** 请求头 */
requestHeaders?: Record<string, string>
/** 请求 ID */
requestId?: string
/** 环境信息 */
environment?: string
/** 重试次数 */
retryCount?: number
}
/**
* 环境信息接口
*/
export interface EnvironmentInfo {
/** 是否为生产环境 */
isProduction: boolean
/** 运行环境模式 */
mode: string
/** 平台信息 */
platform: string
/** 错误监控是否启用 */
errorMonitorEnabled: boolean
/** 时间戳 */
timestamp: number
}
/**
* 格式化函数类型
*/
export type FormatterFunction = (error: ErrorInfo) => string
/**
* 发送器函数类型
*/
export type SenderFunction = (payload: ErrorInfo) => Promise<void>
/**
* 工具函数接口
*/
export interface Utils {
/** 获取当前页面名称 */
getCurrentPageName: () => string
/** 获取当前 URL */
getCurrentUrl: () => string
/** 获取用户代理 */
getUserAgent: () => string
/** 序列化错误对象 */
serializeError: (error: any) => any
}
/**
* 构造函数选项(用于类实例化)
*/
export interface ConstructorOptions extends ErrorMonitorOptions {
/** 配置选项 */
config?: Partial<ErrorMonitorOptions>
}
/**
* Webhook 消息格式
*/
export interface WebhookMessage {
msgtype: 'text'
text: {
content: string
mentioned_list?: string[]
}
}
/**
* UniApp 环境信息
*/
export interface UniAppSystemInfo {
/** 应用名称 */
appName?: string
/** 应用版本 */
appVersion?: string
/** 平台信息 */
platform?: string
/** 系统信息 */
system?: string
/** 设备型号 */
model?: string
/** 小程序模式 */
mode?: string
}

342
src/utils.js Normal file
View File

@@ -0,0 +1,342 @@
/**
* 工具函数
* 提供环境检测、错误序列化等通用功能
*/
/**
* 获取当前页面名称
* @returns {string} 页面名称
*/
export function getCurrentPageName() {
try {
// 尝试从 getCurrentPages 获取(小程序环境)
if (typeof getCurrentPages !== 'undefined') {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
return currentPage.route || currentPage.$page?.fullPath || '未知页面'
}
}
} catch (error) {
// 忽略错误,返回默认值
}
// Web 环境
try {
if (typeof window !== 'undefined' && window.location) {
return window.location.pathname || '未知页面'
}
} catch (error) {
return '未知页面'
}
// UniApp 环境
try {
if (typeof uni !== 'undefined') {
const pages = getCurrentPages?.()
if (pages && pages.length > 0) {
return pages[pages.length - 1]?.route || '未知页面'
}
}
} catch (error) {
return '未知页面'
}
return '未知页面'
}
/**
* 获取当前 URL
* @returns {string} 当前 URL
*/
export function getCurrentUrl() {
// Web 环境
if (typeof window !== 'undefined' && window.location?.href) {
return window.location.href
}
// UniApp 小程序环境
if (typeof uni !== 'undefined') {
try {
const pages = getCurrentPages?.()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
return currentPage?.route || ''
}
} catch (error) {
// 忽略错误
}
}
return ''
}
/**
* 获取用户代理信息
* @returns {string} 用户代理信息
*/
export function getUserAgent() {
// Web 环境
if (typeof navigator !== 'undefined' && navigator.userAgent) {
return navigator.userAgent
}
// UniApp 环境
if (typeof uni !== 'undefined') {
try {
const systemInfo = uni.getSystemInfoSync?.()
if (systemInfo) {
return `${systemInfo.platform || 'Unknown'} ${systemInfo.system || 'Unknown'} ${systemInfo.model || 'Unknown'}`
}
} catch (error) {
// 忽略错误
}
}
return 'Unknown Device'
}
/**
* 序列化错误对象
* @param {any} error 要序列化的错误对象
* @returns {any} 序列化后的错误对象
*/
export function serializeError(error) {
if (error instanceof Error) {
return {
name: error.name || error.code,
message: error.message,
stack: error.stack,
}
}
if (typeof error === 'object' && error !== null) {
try {
return JSON.stringify(error, null, 2)
} catch (e) {
return String(error)
}
}
return String(error)
}
/**
* 检测是否为生产环境
* @returns {boolean} 是否为生产环境
*/
export function isProduction() {
try {
// 检查 uniapp 运行模式
if (typeof uni !== 'undefined') {
const systemInfo = uni.getSystemInfoSync?.()
if (systemInfo?.mode && systemInfo.mode !== 'default') {
// 体验版、开发版、预览版 - 认为是非生产环境
return false
}
}
} catch (error) {
// 忽略错误,继续检测
}
// 检查环境变量 MODE
try {
if (typeof import !== 'undefined' && import.meta?.env?.MODE === 'development') {
return false
}
} catch (error) {
// 忽略错误,继续检测
}
// 检查是否为浏览器环境
if (typeof window !== 'undefined') {
try {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return false
}
} catch (error) {
// 忽略错误
}
}
// 默认:开发环境和体验版不启用,生产环境启用
return true
}
/**
* 检测是否为 UniApp 环境
* @returns {boolean} 是否为 UniApp 环境
*/
export function isUniAppEnvironment() {
return typeof uni !== 'undefined' && typeof getCurrentPages !== 'undefined'
}
/**
* 检测是否为微信小程序环境
* @returns {boolean} 是否为微信小程序环境
*/
export function isWeChatMiniProgram() {
if (typeof uni !== 'undefined' && uni.getSystemInfoSync) {
try {
const systemInfo = uni.getSystemInfoSync()
return systemInfo.hostName === 'wechat' || systemInfo.platform === 'devtools'
} catch (error) {
// 忽略错误
}
}
return false
}
/**
* 延迟执行函数
* @param {number} ms 延迟时间(毫秒)
* @returns {Promise<void>} Promise 对象
*/
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 重试函数
* @param {Function} fn 要执行的函数
* @param {number} maxRetries 最大重试次数
* @param {number} delayTime 重试延迟时间(毫秒)
* @returns {Promise} 执行结果
*/
export async function retry(fn, maxRetries = 3, delayTime = 1000) {
let lastError
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error
if (i < maxRetries) {
await delay(delayTime * (i + 1))
}
}
}
throw lastError
}
/**
* 获取系统信息
* @returns {Object} 系统信息对象
*/
export function getSystemInfo() {
const systemInfo = {}
try {
if (typeof uni !== 'undefined' && uni.getSystemInfoSync) {
Object.assign(systemInfo, uni.getSystemInfoSync())
}
} catch (error) {
// 忽略错误
}
try {
if (typeof navigator !== 'undefined') {
systemInfo.userAgent = navigator.userAgent
systemInfo.language = navigator.language
}
} catch (error) {
// 忽略错误
}
try {
if (typeof window !== 'undefined' && window.location) {
systemInfo.url = window.location.href
systemInfo.hostname = window.location.hostname
systemInfo.pathname = window.location.pathname
}
} catch (error) {
// 忽略错误
}
return systemInfo
}
/**
* 验证 webhook URL 格式
* @param {string} url 要验证的 URL
* @returns {boolean} URL 是否有效
*/
export function isValidWebhookUrl(url) {
if (!url || typeof url !== 'string') {
return false
}
try {
const urlObj = new URL(url)
return ['http:', 'https:'].includes(urlObj.protocol)
} catch (error) {
return false
}
}
/**
* 深度克隆对象
* @param {any} obj 要克隆的对象
* @returns {any} 克隆后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (obj instanceof Date) {
return new Date(obj.getTime())
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item))
}
if (typeof obj === 'object') {
const clonedObj = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}
return obj
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} limit 限制时间(毫秒)
* @returns {Function} 节流后的函数
*/
export function throttle(func, limit) {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}