diff --git a/README.md b/README.md index 5f05d27..7e51aa8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # uniapp-error-monitor -UniApp 错误监控上报插件 - 专业的 JavaScript 错误监控和上报解决方案 +🔍 UniApp 专业错误监控和上报工具 - 支持全平台、多场景错误捕获 [![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) @@ -8,17 +8,18 @@ UniApp 错误监控上报插件 - 专业的 JavaScript 错误监控和上报解 [![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 支持的所有平台 +- 🎯 **零配置使用**: 开箱即用,支持多种导入方式 +- 🔍 **全面错误捕获**: 全局错误、Promise错误、控制台错误、网络错误、小程序错误 +- 🧠 **环境智能**: 自动检测生产环境,非生产环境优雅降级 +- ⚡ **高性能**: 异步发送错误,不阻塞主线程 +- 🔄 **重试机制**: 网络失败自动重试,可配置次数和间隔 +- 📊 **错误统计**: 内置统计功能,便于数据分析 +- 🔧 **高度可定制**: 支持自定义发送器和格式化函数 +- 📱 **全平台支持**: H5、微信小程序、App、支付宝小程序等 - 🛡️ **类型安全**: 完整的 TypeScript 类型定义 -- 📦 **模块化**: 支持 ESM、CommonJS、UMD 多种模块格式 +- 📦 **多格式输出**: 支持 ESM、CommonJS、UMD 格式 ## 📦 安装 @@ -35,10 +36,10 @@ pnpm add uniapp-error-monitor ## 🚀 快速开始 -### 基础使用 +### 方式一:命名导出(推荐) ```javascript -import { initErrorMonitor } from 'uniapp-error-monitor' +import { initErrorMonitor, reportError, getErrorStats, wrapPromise } from 'uniapp-error-monitor' // 初始化错误监控 initErrorMonitor({ @@ -49,56 +50,30 @@ initErrorMonitor({ }) // 手动上报错误 -import { reportError } from 'uniapp-error-monitor' - reportError('manual', new Error('自定义错误'), { page: 'index', action: '用户操作失败' }) -``` -### Promise 包装 - -```javascript -import { wrapPromise } from 'uniapp-error-monitor' - -// 自动捕获 Promise 错误 +// Promise 包装(自动捕获 Promise 错误) const result = await wrapPromise( fetch('https://api.example.com/data') ) + +// 获取错误统计 +const stats = getErrorStats() +console.log('错误统计:', stats) ``` -## 📋 配置选项 - -```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 // 自定义发送器 -} -``` - -## 🔧 高级使用 - -### 自定义发送器 +### 方式二:类实例(高级用法) ```javascript import { ErrorMonitor } from 'uniapp-error-monitor' +// 创建自定义实例 const errorMonitor = new ErrorMonitor() -// 使用自定义发送器 +// 设置自定义发送器 errorMonitor.setSender(async (errorInfo) => { // 发送到自己的服务器 await fetch('/api/errors', { @@ -108,18 +83,6 @@ errorMonitor.setSender(async (errorInfo) => { }) }) -errorMonitor.init({ - webhookUrl: 'custom-sender' // 使用自定义发送器时,webhookUrl 可以设置为任意值 -}) -``` - -### 自定义错误格式化 - -```javascript -import { ErrorMonitor } from 'uniapp-error-monitor' - -const errorMonitor = new ErrorMonitor() - // 设置自定义格式化函数 errorMonitor.setFormatter((errorInfo) => { return `🔴 错误详情: @@ -129,56 +92,181 @@ errorMonitor.setFormatter((errorInfo) => { 时间:${new Date(errorInfo.timestamp).toLocaleString()}` }) -errorMonitor.init({ +// 初始化 +errorMonitor.initErrorMonitor({ webhookUrl: 'your-webhook-url' }) + +// 使用实例方法 +errorMonitor.reportError('api', new Error('接口调用失败')) +``` + +### 方式三:默认实例(向后兼容) + +```javascript +import ErrorMonitor from 'uniapp-error-monitor' + +// 使用默认实例 +ErrorMonitor.initErrorMonitor({ + webhookUrl: 'https://your-webhook-url.com' +}) + +ErrorMonitor.reportError('manual', new Error('测试错误')) +``` + +## ⚙️ 配置选项 + +```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 // 强制启用错误监控(忽略环境检查) + sender?: (errorInfo: ErrorInfo) => Promise // 自定义发送器 + formatter?: (errorInfo: ErrorInfo) => string // 自定义格式化函数 +} +``` + +## 📊 错误类型 + +| 类型 | 说明 | 自动捕获 | 手动上报 | 触发场景 | +|------|------|----------|----------|----------| +| `global` | 全局 JavaScript 错误 | ✅ | ❌ | `window.onerror` | +| `promise` | Promise 拒绝错误 | ✅ | ❌ | `unhandledrejection` | +| `console` | console.error 输出 | ✅ | ❌ | 启用后自动捕获 | +| `miniProgram` | 小程序特定错误 | ✅ | ❌ | `uni.onError`, `uni.onPageNotFound` | +| `network` | 网络请求失败 | ✅ | ❌ | 拦截的 `uni.request` 失败 | +| `api` | API 接口错误 | ❌ | ✅ | 手动调用 `reportError` | +| `manual` | 手动上报错误 | ❌ | ✅ | 手动调用 `reportError` | + +## 🔧 高级用法 + +### 环境检测 + +```javascript +import { getEnvironmentInfo } from 'uniapp-error-monitor' + +const envInfo = getEnvironmentInfo() + +if (envInfo.isProduction) { + console.log('生产环境,错误监控已启用') +} else { + console.log('开发环境,错误监控已禁用') +} +``` + +### 重置统计 + +```javascript +import { resetErrorStats } from 'uniapp-error-monitor' + +// 重置错误统计(在页面刷新或特定事件后) +resetErrorStats() +console.log('错误统计已重置') +``` + +### 丰富的错误上下文 + +```javascript +reportError('global', new Error('页面崩溃'), { + // 用户信息 + userId: 'user123', + userAgent: navigator.userAgent, + + // 页面信息 + currentPage: getCurrentPageName(), + routeParams: getCurrentPage()?.$page?.fullPath, + + // 业务信息 + action: '用户点击按钮', + component: 'UserProfile', + + // 性能信息 + loadTime: performance.now(), + memoryUsage: performance.memory?.usedJSHeapSize, + + // 自定义数据 + customData: { + sessionId: getSessionId(), + feature: 'user_management' + } +}) ``` -### 获取错误统计 +### 批量错误处理 ```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 -} -*/ +// 定时检查错误状态 +setInterval(() => { + const stats = getErrorStats() + if (stats.total > 10) { + console.warn('检测到大量错误:', stats) + // 可以发送告警或执行其他处理逻辑 + } +}, 60000) // 每分钟检查一次 ``` -## 📊 错误类型说明 +## 🛡️ TypeScript 支持 -| 错误类型 | 说明 | 触发条件 | -|---------|------|---------| -| `global` | 全局 JavaScript 错误 | `window.onerror` 捕获 | -| `promise` | 未处理的 Promise 拒绝 | `unhandledrejection` 事件 | -| `console` | console.error 输出 | 手动启用后捕获 | -| `miniProgram` | 小程序特定错误 | `uni.onError`, `uni.onPageNotFound` | -| `network` | 网络请求失败 | 拦截的 `uni.request` 失败 | -| `api` | API 接口错误 | 手动上报的接口错误 | -| `manual` | 手动上报的错误 | 手动调用 `reportError` | +完整的类型安全支持: -## 🏗️ 构建配置 +```typescript +import type { + ErrorMonitorOptions, + ErrorType, + ErrorStats, + EnvironmentInfo, + ErrorInfo +} from 'uniapp-error-monitor' + +// 类型安全的配置 +const options: ErrorMonitorOptions = { + webhookUrl: 'https://example.com/webhook', + enableGlobalError: true, + enablePromiseError: true, + maxRetries: 5, + retryDelay: 2000, +} + +// 类型安全的错误上报 +const reportTypeSafeError = (type: ErrorType, message: string) => { + reportError(type, new Error(message), { + timestamp: Date.now(), + userId: '12345' + }) +} +``` + +## 📱 平台兼容性 + +- ✅ **微信小程序**: 完整支持,包括所有错误类型 +- ✅ **H5**: 完整支持,支持所有现代浏览器 +- ✅ **App (iOS/Android)**: 完整支持 +- ✅ **支付宝小程序**: 基本支持 +- ✅ **字节跳动小程序**: 基本支持 +- ✅ **百度小程序**: 基本支持 +- ✅ **快应用**: 基本支持 + +## 🏗️ 环境配置 ### 环境变量 在你的项目中设置环境变量: -```javascript -// .env 文件 +```bash +# .env 文件 VITE_WEBHOOK=https://your-webhook-url.com ``` -### 开发环境自动禁用 +### 环境检测逻辑 插件会在以下情况下自动禁用(非生产环境): @@ -188,36 +276,22 @@ VITE_WEBHOOK=https://your-webhook-url.com 如需强制启用,设置 `forceEnable: true`。 -## 🔍 TypeScript 支持 +## 📦 构建产物 -完整的 TypeScript 类型支持: +构建后会在 `dist/` 目录生成: -```typescript -import { ErrorMonitor, ErrorMonitorOptions, ErrorType, ErrorStats } from 'uniapp-error-monitor' +- `index.js` - CommonJS 格式(Node.js) +- `index.esm.js` - ES Module 格式(现代构建工具) +- `index.umd.js` - UMD 格式(浏览器直接使用) +- `index.umd.min.js` - UMD 压缩版 +- `index.d.ts` - TypeScript 类型声明 +- `*.map` - Source map 文件 -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 +git clone https://github.com/your-username/uniapp-error-monitor.git cd uniapp-error-monitor # 安装依赖 @@ -229,28 +303,24 @@ npm run dev # 类型检查 npm run type-check +# 代码检查 +npm run lint + # 构建 npm run build -# 单元测试 -npm run test +# 发布 +npm publish ``` -## 📦 构建产物 +## 📄 使用示例 -构建后会在 `dist/` 目录生成: +完整的使用示例请参考 [USAGE_EXAMPLES.js](./USAGE_EXAMPLES.js) 文件。 -- `index.js` - CommonJS 格式 -- `index.mjs` - ES Module 格式 -- `index.umd.js` - UMD 格式(用于浏览器) -- `index.umd.min.js` - UMD 压缩版 -- `index.d.ts` - TypeScript 类型声明 -- `types/` - 类型声明目录 +## 🤝 贡献指南 + +欢迎提交 Issue 和 Pull Request! ## 📄 许可证 -MIT License - 详见 [LICENSE](LICENSE) 文件 - ---- - -⭐ 如果这个项目对你有帮助,请给它一个星标! \ No newline at end of file +MIT License - 详见 [LICENSE](./LICENSE) 文件 \ No newline at end of file diff --git a/USAGE_EXAMPLES.js b/USAGE_EXAMPLES.js new file mode 100644 index 0000000..40109e2 --- /dev/null +++ b/USAGE_EXAMPLES.js @@ -0,0 +1,236 @@ +/** + * UniApp Error Monitor 使用示例 + * 展示如何在不同场景下使用错误监控工具 + */ + +// ============================================================================ +// 1. 基础使用 - 命名导出方式(推荐) +// ============================================================================ + +// 方式一:导入所有命名导出 +import { + initErrorMonitor, + reportError, + getErrorStats, + wrapPromise +} from 'uniapp-error-monitor' + +// 初始化错误监控 +initErrorMonitor({ + webhookUrl: 'https://your-webhook-url.com', // 必填 + enableGlobalError: true, // 启用全局错误捕获 + enablePromiseError: true, // 启用 Promise 错误捕获 + enableConsoleError: false, // 禁用 console.error 捕获 + maxRetries: 3, // 最大重试次数 + retryDelay: 1000, // 重试延迟时间(ms) +}) + +// 手动上报错误 +reportError('manual', new Error('自定义错误'), { + page: 'index', + action: '用户操作失败' +}) + +// 获取错误统计 +const stats = getErrorStats() +console.log('错误统计:', stats) + +// Promise 包装示例 +const fetchData = async () => { + try { + const response = await wrapPromise( + fetch('https://api.example.com/data') + ) + return response.json() + } catch (error) { + // 错误已经被自动捕获和上报 + console.error('获取数据失败:', error) + } +} + +// ============================================================================ +// 2. 高级使用 - 类实例方式 +// ============================================================================ + +import { ErrorMonitor } from 'uniapp-error-monitor' + +// 创建自定义实例 +const errorMonitor = new ErrorMonitor({ + webhookUrl: 'https://your-webhook-url.com', + enableGlobalError: true, + enablePromiseError: true, + forceEnable: true // 强制启用(忽略环境检查) +}) + +// 设置自定义发送器 +errorMonitor.setSender(async (errorInfo) => { + // 发送到自己的服务器 + await fetch('/api/errors', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(errorInfo) + }) +}) + +// 设置自定义格式化函数 +errorMonitor.setFormatter((errorInfo) => { + return `🔴 错误详情: + 类型:${errorInfo.type} + 消息:${errorInfo.error} + 页面:${errorInfo.page} + 时间:${new Date(errorInfo.timestamp).toLocaleString()}` +}) + +// 初始化 +errorMonitor.initErrorMonitor() + +// 使用实例方法 +errorMonitor.reportError('api', new Error('接口调用失败'), { + api: '/user/profile', + method: 'GET' +}) + +// ============================================================================ +// 3. 向后兼容 - 默认实例 +// ============================================================================ + +import ErrorMonitorDefault from 'uniapp-error-monitor' + +// 使用默认实例(向后兼容) +ErrorMonitorDefault.initErrorMonitor({ + webhookUrl: 'https://your-webhook-url.com' +}) + +ErrorMonitorDefault.reportError('manual', new Error('测试错误')) + +// ============================================================================ +// 4. TypeScript 类型安全使用 +// ============================================================================ + +import type { + ErrorMonitorOptions, + ErrorType, + ErrorStats, + EnvironmentInfo +} from 'uniapp-error-monitor' + +// 类型安全的配置 +const options: ErrorMonitorOptions = { + webhookUrl: 'https://example.com/webhook', + enableGlobalError: true, + enablePromiseError: true, + maxRetries: 5, + retryDelay: 2000, + forceEnable: false +} + +// 类型安全的错误上报 +const reportTypeSafeError = (type: ErrorType, message: string) => { + reportError(type, new Error(message), { + timestamp: Date.now(), + userId: '12345' + }) +} + +// 类型安全的统计获取 +const getSafeStats = (): ErrorStats => { + return getErrorStats() +} + +// ============================================================================ +// 5. 环境检测和使用 +// ============================================================================ + +import { getEnvironmentInfo } from 'uniapp-error-monitor' + +// 获取环境信息 +const envInfo: EnvironmentInfo = getEnvironmentInfo() + +if (envInfo.isProduction) { + // 生产环境逻辑 + console.log('生产环境,错误监控已启用') +} else { + // 开发环境逻辑 + console.log('开发环境,错误监控已禁用') +} + +// ============================================================================ +// 6. 错误类型示例 +// ============================================================================ + +// 全局错误 - 自动捕获,无需手动上报 +// window.onerror 触发 + +// Promise 错误 - 通过 wrapPromise 包装或自动捕获 +wrapPromise(someAsyncFunction()) + +// Console 错误 - 需要 enableConsoleError: true +console.error('这条错误会被捕获') + +// 小程序错误 - 自动捕获 +// uni.onError, uni.onPageNotFound 触发 + +// 网络错误 - 自动捕获 +// 拦截的 uni.request 失败 + +// API 错误 - 手动上报 +reportError('api', new Error('接口调用失败'), { + url: '/api/users', + method: 'GET', + statusCode: 500 +}) + +// 手动错误 - 手动上报 +reportError('manual', new Error('用户操作失败'), { + action: 'submitForm', + formData: { name: 'John' } +}) + +// ============================================================================ +// 7. 错误上下文信息 +// ============================================================================ + +// 添加丰富的错误上下文 +reportError('global', new Error('页面崩溃'), { + // 用户信息 + userId: 'user123', + userAgent: navigator.userAgent, + + // 页面信息 + currentPage: getCurrentPageName(), + routeParams: getCurrentPage()?.$page?.fullPath, + + // 业务信息 + action: '用户点击按钮', + component: 'UserProfile', + + // 性能信息 + loadTime: performance.now(), + memoryUsage: performance.memory?.usedJSHeapSize, + + // 自定义数据 + customData: { + sessionId: getSessionId(), + feature: 'user_management' + } +}) + +// ============================================================================ +// 8. 批量错误处理 +// ============================================================================ + +// 重置错误统计(在页面刷新或特定事件后) +const resetStats = () => { + import { resetErrorStats } from 'uniapp-error-monitor' + resetErrorStats() + console.log('错误统计已重置') +} + +// 定时检查错误状态 +setInterval(() => { + const stats = getErrorStats() + if (stats.total > 10) { + console.warn('检测到大量错误:', stats) + // 可以发送告警或执行其他处理逻辑 + } +}, 60000) // 每分钟检查一次 \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100644 index 28d62e6..0000000 --- a/build.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/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!" \ No newline at end of file diff --git a/package.json b/package.json index 16a3b59..b86dd96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniapp-error-monitor", - "version": "1.0.1", + "version": "1.1.0", "description": "专门为UniApp环境设计的错误监控和上报工具,支持全局错误捕获、Promise错误捕获、网络错误捕获等", "main": "dist/index.js", "module": "dist/index.esm.js", @@ -40,9 +40,9 @@ "build": "rollup -c", "dev": "rollup -c -w", "clean": "rimraf dist", - "lint": "eslint src/**/*.js", - "lint:fix": "eslint src/**/*.js --fix", - "prepublishOnly": "npm run clean && npm run build" + "type-check": "tsc --noEmit", + "prepublishOnly": "npm run clean && npm run type-check && npm run build", + "prepare": "npm run build" }, "devDependencies": { "@babel/core": "^7.28.5", diff --git a/rollup.config.js b/rollup.config.js index f0ffc62..19e1c1a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -13,6 +13,25 @@ const external = [ 'uniapp' ] +// 复制 TypeScript 定义文件到 dist 目录 +function copyTypescriptDefinitions() { + return { + name: 'copy-types', + writeBundle() { + const fs = require('fs') + const path = require('path') + + const srcTypesFile = path.resolve(__dirname, 'src/index.d.ts') + const destTypesFile = path.resolve(__dirname, 'dist/index.d.ts') + + if (fs.existsSync(srcTypesFile)) { + fs.copyFileSync(srcTypesFile, destTypesFile) + console.log('✅ TypeScript definitions copied to dist/index.d.ts') + } + } + } +} + const plugins = [ typescript(), babel({ @@ -27,7 +46,8 @@ const plugins = [ } }] ] - }) + }), + copyTypescriptDefinitions() ] export default defineConfig([ @@ -38,7 +58,8 @@ export default defineConfig([ { file: pkg.module, format: 'es', - sourcemap: true + sourcemap: true, + exports: 'named' }, // UMD 输出 (用于浏览器) { @@ -48,13 +69,15 @@ export default defineConfig([ sourcemap: true, globals: { 'vue': 'Vue' - } + }, + exports: 'named' }, // CommonJS 输出 (用于 Node.js) { file: pkg.main, format: 'cjs', - sourcemap: true + sourcemap: true, + exports: 'named' } ], external, @@ -69,6 +92,7 @@ export default defineConfig([ format: 'umd', name: 'UniAppErrorMonitor', sourcemap: false, + exports: 'named', plugins: [terser()] } ], diff --git a/src/ErrorMonitor.d.ts b/src/ErrorMonitor.d.ts deleted file mode 100644 index cf56346..0000000 --- a/src/ErrorMonitor.d.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * 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(promise: Promise): Promise; - - /** - * 获取错误统计信息 - * @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; -} - -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, -}; \ No newline at end of file diff --git a/src/ErrorMonitor.js b/src/ErrorMonitor.js deleted file mode 100644 index 8186664..0000000 --- a/src/ErrorMonitor.js +++ /dev/null @@ -1,710 +0,0 @@ -/** - * 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') - const systemInfo = uni.getSystemInfoSync?.() - - let message = `🚨 JavaScript错误报告\n` - message += `📦 项目: ${systemInfo.appName || this.projectInfo.name}\n` - message += `🏷️ 版本: ${systemInfo.appVersion || 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() { - try { - if (import.meta?.env?.MODE) { - 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() diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..7bc1cfb --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,237 @@ +/** + * UniApp 错误监控和上报工具类型定义 + * @version 1.0.1 + * @author yuantao + */ + +declare module 'uniapp-error-monitor' { + /** + * 错误类型枚举 + */ + export type ErrorType = + | 'global' + | 'promise' + | 'console' + | 'miniProgram' + | 'network' + | 'api' + | 'manual' + + /** + * 错误统计信息接口 + */ + export interface ErrorStats { + total: number + global: number + promise: number + console: number + miniProgram: number + api: number + network: number + lastErrorTime: number | null + } + + /** + * 错误监控配置选项 + */ + export interface ErrorMonitorOptions { + /** 是否启用全局错误捕获 */ + enableGlobalError?: boolean + /** 是否启用Promise错误捕获 */ + enablePromiseError?: boolean + /** 是否启用console.error捕获 */ + enableConsoleError?: boolean + /** 自定义webhook地址,不传则使用环境变量 */ + webhookUrl?: string + /** 发送失败时最大重试次数 */ + maxRetries?: number + /** 重试延迟时间(毫秒) */ + retryDelay?: number + /** 强制启用错误监控(忽略环境检查) */ + forceEnable?: boolean + /** 自定义发送器 */ + sender?: (errorInfo: ErrorInfo) => Promise + /** 自定义格式化函数 */ + formatter?: (errorInfo: ErrorInfo) => string + } + + /** + * 错误信息接口 + */ + export interface ErrorInfo { + type: ErrorType + error: string | Error | object + stack?: string | null + context?: object + timestamp: number + url?: string + method?: string + userAgent?: string + page?: string + + // API错误特有字段 + statusCode?: number + statusText?: string + responseTime?: number + requestData?: any + requestHeaders?: any + requestId?: string + environment?: string + + // 网络错误特有字段 + retryCount?: number + networkType?: string + isConnected?: boolean + + // Promise错误特有字段 + reason?: any + promise?: any + + // 全局错误特有字段 + message?: string + source?: string + lineno?: number + colno?: number + + // Console错误特有字段 + args?: any[] + + // 小程序错误特有字段 + path?: string + query?: string + } + + /** + * 环境信息接口 + */ + export interface EnvironmentInfo { + isProduction: boolean + mode: string + platform: string + errorMonitorEnabled: boolean + timestamp: number + } + + /** + * 错误监控类 + */ + export class ErrorMonitor { + constructor(options?: ErrorMonitorOptions) + + /** + * 初始化全局错误监控 + * @param options 配置选项 + */ + initErrorMonitor(options?: ErrorMonitorOptions): void + + /** + * 手动上报错误 + * @param type 错误类型 + * @param error 错误对象或错误信息 + * @param context 错误上下文信息 + * @param forceSend 强制发送(忽略环境检查) + */ + reportError( + type?: ErrorType, + error?: Error | string | object, + context?: object, + forceSend?: boolean + ): void + + /** + * 获取错误统计信息 + * @returns 错误统计信息 + */ + getErrorStats(): ErrorStats + + /** + * 重置错误统计 + */ + resetErrorStats(): void + + /** + * 获取当前环境信息 + * @returns 环境信息 + */ + getEnvironmentInfo(): EnvironmentInfo + + /** + * 设置自定义发送器 + * @param sender 自定义发送器函数 + */ + setSender(sender: (errorInfo: ErrorInfo) => Promise): void + + /** + * 设置自定义格式化函数 + * @param formatter 自定义格式化函数 + */ + setFormatter(formatter: (errorInfo: ErrorInfo) => string): void + + /** + * 包装Promise以自动捕获错误 + * @param promise 要包装的Promise + * @returns 包装后的Promise + */ + wrapPromise(promise: Promise): Promise + } + + /** + * 便捷方法 - 初始化错误监控 + * @param options 配置选项 + */ + export function initErrorMonitor(options?: ErrorMonitorOptions): void + + /** + * 便捷方法 - 手动上报错误 + * @param type 错误类型 + * @param error 错误对象或错误信息 + * @param context 错误上下文信息 + * @param forceSend 强制发送(忽略环境检查) + */ + export function reportError( + type?: ErrorType, + error?: Error | string | object, + context?: object, + forceSend?: boolean + ): void + + /** + * 便捷方法 - 获取错误统计信息 + * @returns 错误统计信息 + */ + export function getErrorStats(): ErrorStats + + /** + * 便捷方法 - 重置错误统计 + */ + export function resetErrorStats(): void + + /** + * 便捷方法 - 获取当前环境信息 + * @returns 环境信息 + */ + export function getEnvironmentInfo(): EnvironmentInfo + + /** + * 便捷方法 - 包装Promise以自动捕获错误 + * @param promise 要包装的Promise + * @returns 包装后的Promise + */ + export function wrapPromise(promise: Promise): Promise + + /** + * 默认实例 - 向后兼容 + */ + const errorMonitor: ErrorMonitor + export default errorMonitor +} + +// 全局类型声明(如果需要在全局使用) +declare global { + interface Window { + UniAppErrorMonitor: typeof import('uniapp-error-monitor').default + } + + const uni: any + const getCurrentPages: any +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 519bd69..836e128 100644 --- a/src/index.js +++ b/src/index.js @@ -1,101 +1,729 @@ /** - * UniApp 错误监控上报插件 - 入口文件 - * 提供完整的 JavaScript 错误监控和上报解决方案 + * 错误监控和上报类 */ +class ErrorMonitor { + constructor(options = {}) { + // 初始化错误统计 + this.errorStats = { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + lastErrorTime: null, + } -import ErrorMonitor from './ErrorMonitor' -import { getCurrentPageName, getCurrentUrl, getUserAgent, serializeError } from './utils' + // Promise包装方法 + this.wrapPromise = null -// 创建单例实例 + // 配置信息 + this.config = null + + // 项目信息 + this.projectInfo = { + name: '未命名项目', + version: '0.0.0', + } + + // 尝试从 manifest.json 加载项目信息 + this._loadProjectInfo() + + // 应用初始配置 + if (Object.keys(options).length > 0) { + this.initErrorMonitor(options) + } + } + + /** + * 检测是否为生产环境 + * @private + * @returns {boolean} 是否为生产环境 + */ + _isProduction() { + // 检查uniapp运行模式 + try { + const systemInfo = uni.getSystemInfoSync?.() + if (systemInfo?.mode && systemInfo.mode !== 'default') { + // 体验版、开发版、预览版 + return false + } + } catch (error) { + // 忽略错误,继续检测 + } + + // 检查环境变量MODE + if (import.meta.env.MODE === 'development') { + return false + } + + // 默认:开发环境和体验版不启用,生产环境启用 + return true + } + + /** + * 初始化全局错误监控 + * @param {Object} options 配置选项 + * @param {boolean} [options.enableGlobalError=true] 是否启用全局错误捕获 + * @param {boolean} [options.enablePromiseError=true] 是否启用Promise错误捕获 + * @param {boolean} [options.enableConsoleError=true] 是否启用console.error捕获 + * @param {string} [options.webhookUrl] 自定义webhook地址,不传则使用环境变量 + * @param {number} [options.maxRetries=3] 发送失败时最大重试次数 + * @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒) + * @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查) + */ + initErrorMonitor(options = {}) { + const config = { + enableGlobalError: true, + enablePromiseError: true, + enableConsoleError: false, + webhookUrl: import.meta.env.VITE_WEBHOOK, + maxRetries: 3, + retryDelay: 1000, + forceEnable: false, + ...options, + } + + // 环境检查:只在生产环境下启用错误监控 + if (!config.forceEnable && !this._isProduction()) { + console.info('当前为非生产环境,错误监控已禁用') + return + } + + // 检查webhook配置 + if (!config.webhookUrl) { + console.warn('错误监控初始化失败:未配置webhook地址') + return + } + + this.config = config + + // 全局错误捕获(uniapp环境适配) + if (config.enableGlobalError) { + // 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(), + }) + }) + } + + // uniapp环境 - 提供Promise包装工具 + if (typeof uni !== 'undefined' && config.enablePromiseError) { + // 提供一个包装Promise的方法,让开发者可以手动包装重要的Promise + this.wrapPromise = promise => { + const self = this + return promise.catch(error => { + self._handlePromiseError({ + type: 'promise', + reason: error, + timestamp: Date.now(), + }) + throw error + }) + } + } + } + + // console.error捕获(可选) + if (config.enableConsoleError) { + 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(), + }) + } + } + + // 微信小程序错误捕获 + 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(), + }) + }, + }) + } + } + + console.log('错误监控已初始化') + } + + /** + * 手动上报错误 + * @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: error.url || this._getCurrentUrl(), + method: error.method || '', + userAgent: this._getUserAgent(), + page: 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.errorStats.total++ + this.errorStats[type] = (this.errorStats[type] || 0) + 1 + this.errorStats.lastErrorTime = errorInfo.timestamp + + if (forceSend) { + // 强制发送 + this._sendErrorToWebhook(errorInfo, 0, true) + } else { + this._sendErrorToWebhook(errorInfo) + } + } + + /** + * 获取错误统计信息 + * @returns {Object} 错误统计信息 + */ + getErrorStats() { + return { ...this.errorStats } + } + + /** + * 重置错误统计 + */ + resetErrorStats() { + this.errorStats = { + total: 0, + global: 0, + promise: 0, + console: 0, + miniProgram: 0, + api: 0, + network: 0, + lastErrorTime: null, + } + } + + /** + * 获取当前环境信息 + * @returns {Object} 环境信息 + */ + getEnvironmentInfo() { + return { + isProduction: this._isProduction(), + mode: import.meta.env.MODE, + platform: this._getUserAgent(), + errorMonitorEnabled: !!this.config, + timestamp: Date.now(), + } + } + + /** + * 处理全局错误 + * @private + */ + _handleGlobalError(errorInfo) { + this.errorStats.total++ + this.errorStats.global++ + this.errorStats.lastErrorTime = errorInfo.timestamp + + this._sendErrorToWebhook({ + ...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: getCurrentPageName(), + }) + } + + /** + * 处理Promise错误 + * @private + */ + _handlePromiseError(errorInfo) { + this.errorStats.total++ + this.errorStats.promise++ + this.errorStats.lastErrorTime = errorInfo.timestamp + + this._sendErrorToWebhook({ + ...errorInfo, + reason: this._serializeError(errorInfo.reason), + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: getCurrentPageName(), + }) + } + + /** + * 处理console错误 + * @private + */ + _handleConsoleError(errorInfo) { + this.errorStats.total++ + this.errorStats.console++ + this.errorStats.lastErrorTime = errorInfo.timestamp + + this._sendErrorToWebhook({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: getCurrentPageName(), + }) + } + + /** + * 处理小程序错误 + * @private + */ + _handleMiniProgramError(errorInfo) { + this.errorStats.total++ + this.errorStats.miniProgram++ + this.errorStats.lastErrorTime = errorInfo.timestamp + + this._sendErrorToWebhook({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: getCurrentPageName(), + }) + } + + /** + * 处理网络错误 + * @private + */ + _handleNetworkError(errorInfo) { + this.errorStats.total++ + this.errorStats.network++ + this.errorStats.lastErrorTime = errorInfo.timestamp + + this._sendErrorToWebhook({ + ...errorInfo, + url: this._getCurrentUrl(), + userAgent: this._getUserAgent(), + page: getCurrentPageName(), + }) + } + + /** + * 获取当前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 + */ + _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) + } + + /** + * 发送错误到webhook + * @private + */ + async _sendErrorToWebhook(errorInfo, retryCount = 0, forceSend = false) { + // 环境检查:只在生产环境下发送错误信息 + if (!forceSend && !this._isProduction() && !this.config?.forceEnable) { + console.info('非生产环境,错误信息不上报到webhook:', errorInfo.type) + return + } + + const webhookUrl = import.meta.env.VITE_WEBHOOK + if (!webhookUrl) { + console.error('未配置webhook地址,无法发送错误信息') + return + } + + try { + // 格式化错误信息 + const message = 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, + }) + }) + + console.log('错误信息已发送到webhook') + } catch (error) { + console.error('发送错误到webhook失败:', error) + + // 重试机制 + if (retryCount < (this.config?.maxRetries || 3)) { + setTimeout(() => { + this._sendErrorToWebhook(errorInfo, retryCount + 1) + }, (this.config?.retryDelay || 1000) * (retryCount + 1)) + } + } + } + + /** + * 加载项目信息 + * @private + */ + _loadProjectInfo() { + try { + if (uni.getSystemInfoSync()) { + const AppInfo = uni.getSystemInfoSync() + this.projectInfo.name = AppInfo.appName + this.projectInfo.version = AppInfo.appVersion + } + } catch (error) { + // 如果加载失败,使用默认信息 + console.warn('无法加载项目信息,使用默认值') + } + } + + /** + * 格式化错误消息 + * @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.requestHeaders) { + message += `🔑 请求头: ${this._serializeError(errorInfo.requestHeaders)}\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` + } + + // 如果是标准错误对象格式 + if (errorInfo.error.name || errorInfo.error.code) { + message += `🏷️ 错误名称: ${errorInfo.error.name || errorInfo.error.code}\n` + } + if (errorInfo.error.stack) { + message += `📜 错误堆栈: ${errorInfo.error.stack}\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` + + // 添加设备信息 + if (errorInfo.userAgent) { + message += `\n📱 设备信息:\n${errorInfo.userAgent}\n` + } + + return message + } +} + +/** + * 获取当前页面名称 + * @returns {string} 页面名称 + */ +function getCurrentPageName() { + try { + // 尝试从getCurrentPages获取 + 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 '未知页面' + } + } + + // Web环境 + try { + if (typeof window !== 'undefined' && window.location) { + return window.location.pathname || '未知页面' + } + } catch (error) { + return '未知页面' + } + + return '未知页面' +} + +// 创建默认实例 const errorMonitorInstance = new ErrorMonitor() -/** - * 初始化错误监控 - * @param {Object} options 配置选项 - * @returns {ErrorMonitor} 错误监控实例 - */ -export function initErrorMonitor(options = {}) { - return errorMonitorInstance.init(options) +// 命名导出 - 便捷方法 +export const initErrorMonitor = (options) => { + return errorMonitorInstance.initErrorMonitor(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) +export const reportError = (type, error, context, forceSend) => { + return 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() { +export const getErrorStats = () => { return errorMonitorInstance.getErrorStats() } -/** - * 重置错误统计 - */ -export function resetErrorStats() { - errorMonitorInstance.resetErrorStats() +export const resetErrorStats = () => { + return errorMonitorInstance.resetErrorStats() } -/** - * 获取环境信息 - * @returns {Object} 环境信息 - */ -export function getEnvironmentInfo() { +export const getEnvironmentInfo = () => { return errorMonitorInstance.getEnvironmentInfo() } -/** - * 检查是否为生产环境 - * @returns {boolean} 是否为生产环境 - */ -export function isProduction() { - return errorMonitorInstance.isProduction() +export const wrapPromise = (promise) => { + return errorMonitorInstance.wrapPromise ? errorMonitorInstance.wrapPromise(promise) : promise } -/** - * 设置自定义发送器 - * @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 default errorMonitorInstance \ No newline at end of file +// 默认导出 - 向后兼容 +export default errorMonitorInstance diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 86ab1e9..0000000 --- a/src/types.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * 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 -} - -/** - * 错误类型枚举 - */ -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 - /** 错误时间戳 */ - 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[] - - // 小程序错误特有字段 - /** 错误对象 */ - errorObject?: 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 - /** 请求 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 - -/** - * 工具函数接口 - */ -export interface Utils { - /** 获取当前页面名称 */ - getCurrentPageName: () => string - /** 获取当前 URL */ - getCurrentUrl: () => string - /** 获取用户代理 */ - getUserAgent: () => string - /** 序列化错误对象 */ - serializeError: (error: any) => any -} - -/** - * 构造函数选项(用于类实例化) - */ -export interface ConstructorOptions extends ErrorMonitorOptions { - /** 配置选项 */ - config?: Partial -} - -/** - * 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 -} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index a2347eb..0000000 --- a/src/utils.js +++ /dev/null @@ -1,343 +0,0 @@ -/** - * 工具函数 - * 提供环境检测、错误序列化等通用功能 - */ - -/** - * 获取当前页面名称 - * @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 { - // 使用 process.env 检测环境变量 - if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === '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} 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) - } - } -} \ No newline at end of file