You've already forked uniapp-error-monitor
新增 错误去重功能,相同错误在指定间隔内只上报一次
This commit is contained in:
251
src/index.js
251
src/index.js
@@ -24,6 +24,12 @@ const ERROR_SEVERITY = {
|
|||||||
manual: 'normal', // 手动上报 - 普通
|
manual: 'normal', // 手动上报 - 普通
|
||||||
pageNotFound: 'critical', // 页面未找到 - 严重
|
pageNotFound: 'critical', // 页面未找到 - 严重
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 默认错误去重间隔时间(毫秒)
|
||||||
|
* @constant {number}
|
||||||
|
*/
|
||||||
|
const DEFAULT_DEDUP_INTERVAL = 60 * 1000 // 1分钟
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 错误监控和上报类
|
* 错误监控和上报类
|
||||||
*/
|
*/
|
||||||
@@ -49,14 +55,15 @@ class ErrorMonitor {
|
|||||||
name: '未命名项目',
|
name: '未命名项目',
|
||||||
version: '0.0.0',
|
version: '0.0.0',
|
||||||
}
|
}
|
||||||
|
// 错误去重缓存:存储最近上报的错误签名和时间戳
|
||||||
|
this._errorCache = new Map()
|
||||||
// 尝试从 manifest.json 加载项目信息
|
// 尝试从 manifest.json 加载项目信息
|
||||||
this._loadProjectInfo()
|
this._loadProjectInfo()
|
||||||
// 应用初始配置
|
// 应用初始配置
|
||||||
if (Object.keys(options).length > 0) {
|
if (Object.keys(options).length > 0) {
|
||||||
this.initErrorMonitor(options)
|
this.initErrorMonitor(options)
|
||||||
}
|
}
|
||||||
}
|
} /**
|
||||||
/**
|
|
||||||
* 检测是否为生产环境
|
* 检测是否为生产环境
|
||||||
* @private
|
* @private
|
||||||
* @returns {boolean} 是否为生产环境
|
* @returns {boolean} 是否为生产环境
|
||||||
@@ -90,6 +97,7 @@ class ErrorMonitor {
|
|||||||
* @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒)
|
* @param {number} [options.retryDelay=1000] 重试延迟时间(毫秒)
|
||||||
* @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查)
|
* @param {boolean} [options.forceEnable=false] 强制启用错误监控(忽略环境检查)
|
||||||
* @param {string} [options.errorLevel='standard'] 错误级别:strict(所有错误)、standard(基本错误)、silent(仅严重错误)
|
* @param {string} [options.errorLevel='standard'] 错误级别:strict(所有错误)、standard(基本错误)、silent(仅严重错误)
|
||||||
|
* @param {number} [options.dedupInterval=60000] 相同错误去重间隔时间(毫秒),默认1分钟
|
||||||
*/
|
*/
|
||||||
initErrorMonitor(options = {}) {
|
initErrorMonitor(options = {}) {
|
||||||
const config = {
|
const config = {
|
||||||
@@ -101,9 +109,9 @@ class ErrorMonitor {
|
|||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
forceEnable: false,
|
forceEnable: false,
|
||||||
errorLevel: ERROR_LEVEL.SILENT, // 默认静默模式
|
errorLevel: ERROR_LEVEL.SILENT, // 默认静默模式
|
||||||
|
dedupInterval: DEFAULT_DEDUP_INTERVAL, // 默认1分钟去重间隔
|
||||||
...options,
|
...options,
|
||||||
}
|
} // 环境检查:只在生产环境下启用错误监控
|
||||||
// 环境检查:只在生产环境下启用错误监控
|
|
||||||
if (!config.forceEnable && !this._isProduction()) {
|
if (!config.forceEnable && !this._isProduction()) {
|
||||||
console.info('当前为非生产环境,错误监控已禁用')
|
console.info('当前为非生产环境,错误监控已禁用')
|
||||||
return
|
return
|
||||||
@@ -234,7 +242,18 @@ class ErrorMonitor {
|
|||||||
console.info(`错误级别过滤:跳过上报 ${type} 类型错误`)
|
console.info(`错误级别过滤:跳过上报 ${type} 类型错误`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 自动提取API错误相关信息 let extractedError = error
|
|
||||||
|
// 生成错误签名用于去重
|
||||||
|
const errorSignature = this._generateErrorSignature(type, error, context)
|
||||||
|
|
||||||
|
// 错误去重检查(forceSend 时跳过)
|
||||||
|
if (!forceSend && this._isDuplicateError(errorSignature)) {
|
||||||
|
console.info(`错误去重:跳过重复错误 ${errorSignature}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动提取API错误相关信息
|
||||||
|
let extractedError = error
|
||||||
let extractedContext = context
|
let extractedContext = context
|
||||||
if (type === 'api' && typeof error === 'object' && error.config) {
|
if (type === 'api' && typeof error === 'object' && error.config) {
|
||||||
// 当type为'api'且error对象包含config属性时,自动提取API相关信息
|
// 当type为'api'且error对象包含config属性时,自动提取API相关信息
|
||||||
@@ -370,28 +389,198 @@ class ErrorMonitor {
|
|||||||
console.log(`错误级别已更新为: ${level}`)
|
console.log(`错误级别已更新为: ${level}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理全局错误
|
* 生成错误签名(用于去重)
|
||||||
* @private
|
* @private
|
||||||
|
* @param {string|Object} typeOrErrorInfo 错误类型或错误信息对象
|
||||||
|
* @param {Error|Object} [error] 错误对象(当第一个参数是类型时使用)
|
||||||
|
* @param {Object} [context] 错误上下文(当第一个参数是类型时使用)
|
||||||
|
* @returns {string} 错误签名
|
||||||
*/
|
*/
|
||||||
_handleGlobalError(errorInfo) {
|
_generateErrorSignature(typeOrErrorInfo, error, context) {
|
||||||
// 错误级别过滤
|
// 兼容两种调用方式
|
||||||
if (!this._shouldReportError('global')) {
|
let type, errorInfo
|
||||||
return
|
if (typeof typeOrErrorInfo === 'string') {
|
||||||
|
type = typeOrErrorInfo
|
||||||
|
// 根据类型提取签名所需的关键信息
|
||||||
|
const errorMessage = error instanceof Error ? error.message : (typeof error === 'string' ? error : JSON.stringify(error))
|
||||||
|
const url = context?.url || ''
|
||||||
|
const method = context?.method || ''
|
||||||
|
const statusCode = context?.statusCode || 0
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'global':
|
||||||
|
return `${type}:${errorMessage}:${context?.source || ''}:${context?.lineno || 0}:${context?.colno || 0}`
|
||||||
|
case 'promise':
|
||||||
|
return `${type}:${errorMessage}`
|
||||||
|
case 'console':
|
||||||
|
return `${type}:${errorMessage}`
|
||||||
|
case 'miniProgram':
|
||||||
|
case 'pageNotFound':
|
||||||
|
return `${type}:${errorMessage}:${context?.path || ''}`
|
||||||
|
case 'network':
|
||||||
|
return `${type}:${url}:${method}`
|
||||||
|
case 'api':
|
||||||
|
return `${type}:${url}:${method}:${statusCode}`
|
||||||
|
default:
|
||||||
|
return `${type}:${errorMessage}`
|
||||||
}
|
}
|
||||||
this.errorStats.total++
|
} else {
|
||||||
this.errorStats.global++
|
// 旧的方式:传入 errorInfo 对象
|
||||||
this.errorStats.lastErrorTime = errorInfo.timestamp
|
errorInfo = typeOrErrorInfo
|
||||||
this._sendErrorToWebhook({
|
type = errorInfo.type || 'unknown'
|
||||||
|
let signature = type
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'global':
|
||||||
|
signature = `${type}:${errorInfo.message || ''}:${errorInfo.source || ''}:${errorInfo.lineno || 0}:${errorInfo.colno || 0}`
|
||||||
|
break
|
||||||
|
case 'promise':
|
||||||
|
const reason = typeof errorInfo.reason === 'object'
|
||||||
|
? JSON.stringify(errorInfo.reason)
|
||||||
|
: String(errorInfo.reason || '')
|
||||||
|
signature = `${type}:${reason}`
|
||||||
|
break
|
||||||
|
case 'console':
|
||||||
|
signature = `${type}:${(errorInfo.args || []).join('|')}`
|
||||||
|
break
|
||||||
|
case 'miniProgram':
|
||||||
|
case 'pageNotFound':
|
||||||
|
signature = `${type}:${errorInfo.error || errorInfo.path || ''}`
|
||||||
|
break
|
||||||
|
case 'network':
|
||||||
|
signature = `${type}:${errorInfo.url || ''}:${errorInfo.method || ''}`
|
||||||
|
break
|
||||||
|
case 'api':
|
||||||
|
signature = `${type}:${errorInfo.url || ''}:${errorInfo.method || ''}:${errorInfo.statusCode || 0}`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
signature = `${type}:${errorInfo.error || errorInfo.message || ''}`
|
||||||
|
}
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 检查错误是否在去重间隔内已上报过
|
||||||
|
* @private
|
||||||
|
* @param {string|Object} signatureOrErrorInfo 错误签名或错误信息对象
|
||||||
|
* @returns {boolean} true表示是重复错误(应跳过),false表示是新错误(应上报)
|
||||||
|
*/
|
||||||
|
_isDuplicateError(signatureOrErrorInfo) {
|
||||||
|
// 支持传入签名或 errorInfo 对象
|
||||||
|
const signature = typeof signatureOrErrorInfo === 'string'
|
||||||
|
? signatureOrErrorInfo
|
||||||
|
: this._generateErrorSignature(signatureOrErrorInfo)
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const dedupInterval = this.config?.dedupInterval || DEFAULT_DEDUP_INTERVAL
|
||||||
|
|
||||||
|
// 检查缓存中是否存在该签名
|
||||||
|
if (this._errorCache.has(signature)) {
|
||||||
|
const lastReportTime = this._errorCache.get(signature)
|
||||||
|
|
||||||
|
// 如果在去重间隔内,认为是重复错误
|
||||||
|
if (now - lastReportTime < dedupInterval) {
|
||||||
|
console.info(`错误去重:跳过重复错误,距上次上报 ${Math.round((now - lastReportTime) / 1000)} 秒`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
this._errorCache.set(signature, now)
|
||||||
|
|
||||||
|
// 清理过期的缓存条目(避免内存泄漏)
|
||||||
|
this._cleanupErrorCache(now, dedupInterval)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期的错误缓存
|
||||||
|
* @private
|
||||||
|
* @param {number} now 当前时间戳
|
||||||
|
* @param {number} dedupInterval 去重间隔
|
||||||
|
*/
|
||||||
|
_cleanupErrorCache(now, dedupInterval) {
|
||||||
|
// 当缓存超过100条时进行清理
|
||||||
|
if (this._errorCache.size > 100) {
|
||||||
|
for (const [key, timestamp] of this._errorCache.entries()) {
|
||||||
|
if (now - timestamp > dedupInterval) {
|
||||||
|
this._errorCache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空错误去重缓存
|
||||||
|
*/
|
||||||
|
clearErrorCache() {
|
||||||
|
this._errorCache.clear()
|
||||||
|
console.log('错误去重缓存已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
|
* 处理全局错误
|
||||||
|
|
||||||
|
* @private
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
_handleGlobalError(errorInfo) {
|
||||||
|
|
||||||
|
// 错误级别过滤
|
||||||
|
|
||||||
|
if (!this._shouldReportError('global')) {
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整错误信息用于去重检查
|
||||||
|
|
||||||
|
const fullErrorInfo = {
|
||||||
|
|
||||||
...errorInfo,
|
...errorInfo,
|
||||||
|
|
||||||
message: errorInfo.message || 'Unknown global error',
|
message: errorInfo.message || 'Unknown global error',
|
||||||
|
|
||||||
source: errorInfo.source || '',
|
source: errorInfo.source || '',
|
||||||
|
|
||||||
lineno: errorInfo.lineno || 0,
|
lineno: errorInfo.lineno || 0,
|
||||||
|
|
||||||
colno: errorInfo.colno || 0,
|
colno: errorInfo.colno || 0,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误去重检查
|
||||||
|
|
||||||
|
if (this._isDuplicateError(fullErrorInfo)) {
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errorStats.total++
|
||||||
|
|
||||||
|
this.errorStats.global++
|
||||||
|
|
||||||
|
this.errorStats.lastErrorTime = errorInfo.timestamp
|
||||||
|
|
||||||
|
this._sendErrorToWebhook({
|
||||||
|
|
||||||
|
...fullErrorInfo,
|
||||||
|
|
||||||
url: this._getCurrentUrl(),
|
url: this._getCurrentUrl(),
|
||||||
|
|
||||||
userAgent: this._getUserAgent(),
|
userAgent: this._getUserAgent(),
|
||||||
|
|
||||||
page: getCurrentPageName(),
|
page: getCurrentPageName(),
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 处理Promise错误
|
* 处理Promise错误
|
||||||
@@ -402,18 +591,25 @@ class ErrorMonitor {
|
|||||||
if (!this._shouldReportError('promise')) {
|
if (!this._shouldReportError('promise')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 构建完整错误信息用于去重检查
|
||||||
|
const fullErrorInfo = {
|
||||||
|
...errorInfo,
|
||||||
|
reason: this._serializeError(errorInfo.reason),
|
||||||
|
}
|
||||||
|
// 错误去重检查
|
||||||
|
if (this._isDuplicateError(fullErrorInfo)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.errorStats.total++
|
this.errorStats.total++
|
||||||
this.errorStats.promise++
|
this.errorStats.promise++
|
||||||
this.errorStats.lastErrorTime = errorInfo.timestamp
|
this.errorStats.lastErrorTime = errorInfo.timestamp
|
||||||
this._sendErrorToWebhook({
|
this._sendErrorToWebhook({
|
||||||
...errorInfo,
|
...fullErrorInfo,
|
||||||
reason: this._serializeError(errorInfo.reason),
|
|
||||||
url: this._getCurrentUrl(),
|
url: this._getCurrentUrl(),
|
||||||
userAgent: this._getUserAgent(),
|
userAgent: this._getUserAgent(),
|
||||||
page: getCurrentPageName(),
|
page: getCurrentPageName(),
|
||||||
})
|
})
|
||||||
}
|
} /**
|
||||||
/**
|
|
||||||
* 处理console错误
|
* 处理console错误
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@@ -422,6 +618,10 @@ class ErrorMonitor {
|
|||||||
if (!this._shouldReportError('console')) {
|
if (!this._shouldReportError('console')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 错误去重检查
|
||||||
|
if (this._isDuplicateError(errorInfo)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.errorStats.total++
|
this.errorStats.total++
|
||||||
this.errorStats.console++
|
this.errorStats.console++
|
||||||
this.errorStats.lastErrorTime = errorInfo.timestamp
|
this.errorStats.lastErrorTime = errorInfo.timestamp
|
||||||
@@ -442,6 +642,10 @@ class ErrorMonitor {
|
|||||||
if (!this._shouldReportError(errorType)) {
|
if (!this._shouldReportError(errorType)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 错误去重检查
|
||||||
|
if (this._isDuplicateError(errorInfo)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.errorStats.total++
|
this.errorStats.total++
|
||||||
this.errorStats.miniProgram++
|
this.errorStats.miniProgram++
|
||||||
this.errorStats.lastErrorTime = errorInfo.timestamp
|
this.errorStats.lastErrorTime = errorInfo.timestamp
|
||||||
@@ -461,6 +665,10 @@ class ErrorMonitor {
|
|||||||
if (!this._shouldReportError('network')) {
|
if (!this._shouldReportError('network')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 错误去重检查
|
||||||
|
if (this._isDuplicateError(errorInfo)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.errorStats.total++
|
this.errorStats.total++
|
||||||
this.errorStats.network++
|
this.errorStats.network++
|
||||||
this.errorStats.lastErrorTime = errorInfo.timestamp
|
this.errorStats.lastErrorTime = errorInfo.timestamp
|
||||||
@@ -781,10 +989,13 @@ export const wrapPromise = promise => {
|
|||||||
return errorMonitorInstance.wrapPromise ? errorMonitorInstance.wrapPromise(promise) : promise
|
return errorMonitorInstance.wrapPromise ? errorMonitorInstance.wrapPromise(promise) : promise
|
||||||
}
|
}
|
||||||
export const getErrorLevel = () => {
|
export const getErrorLevel = () => {
|
||||||
return errorMonitorInstance.getErrorLevel()
|
return errorMonitorInstance.getErrorLevel()
|
||||||
}
|
}
|
||||||
export const setErrorLevel = level => {
|
export const setErrorLevel = level => {
|
||||||
return errorMonitorInstance.setErrorLevel(level)
|
return errorMonitorInstance.setErrorLevel(level)
|
||||||
|
}
|
||||||
|
export const clearErrorCache = () => {
|
||||||
|
return errorMonitorInstance.clearErrorCache()
|
||||||
}
|
}
|
||||||
// 默认导出 - 向后兼容
|
// 默认导出 - 向后兼容
|
||||||
export default errorMonitorInstance
|
export default errorMonitorInstance
|
||||||
|
|||||||
314
test/error-monitor.test.js
Normal file
314
test/error-monitor.test.js
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* 错误监控功能测试文件
|
||||||
|
* 运行方式:node test/error-monitor.test.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 模拟 uni 环境
|
||||||
|
global.uni = {
|
||||||
|
getSystemInfoSync: () => ({
|
||||||
|
appName: '测试应用',
|
||||||
|
appVersion: '1.0.0',
|
||||||
|
platform: 'windows',
|
||||||
|
system: 'Windows 10',
|
||||||
|
model: 'PC',
|
||||||
|
mode: 'default',
|
||||||
|
}),
|
||||||
|
request: (options) => {
|
||||||
|
console.log(`[模拟请求] ${options.method} ${options.url}`)
|
||||||
|
console.log('[请求内容]', JSON.stringify(options.data, null, 2))
|
||||||
|
// 模拟成功响应
|
||||||
|
setTimeout(() => {
|
||||||
|
if (options.success) {
|
||||||
|
options.success({
|
||||||
|
statusCode: 200,
|
||||||
|
data: { errcode: 0, errmsg: 'ok' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
return { abort: () => {} }
|
||||||
|
},
|
||||||
|
onError: (callback) => {
|
||||||
|
global._uniOnErrorCallback = callback
|
||||||
|
},
|
||||||
|
onPageNotFound: (callback) => {
|
||||||
|
global._uniOnPageNotFoundCallback = callback
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟 getCurrentPages
|
||||||
|
global.getCurrentPages = () => [
|
||||||
|
{
|
||||||
|
route: 'pages/index/index',
|
||||||
|
$page: { fullPath: '/pages/index/index' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 模拟 window 环境
|
||||||
|
global.window = {
|
||||||
|
location: { href: 'http://localhost:8080/test' },
|
||||||
|
onerror: null,
|
||||||
|
addEventListener: (event, callback) => {
|
||||||
|
if (event === 'unhandledrejection') {
|
||||||
|
global._unhandledRejectionCallback = callback
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
global.navigator = {
|
||||||
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test Browser'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置环境变量
|
||||||
|
process.env.MODE = 'production'
|
||||||
|
process.env.VITE_WEBHOOK = ''
|
||||||
|
|
||||||
|
// 读取源文件并修改 import.meta 引用
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// 读取源代码
|
||||||
|
const sourcePath = path.join(__dirname, '../src/index.js')
|
||||||
|
let sourceCode = fs.readFileSync(sourcePath, 'utf-8')
|
||||||
|
|
||||||
|
// 替换 import.meta.env 为 process.env
|
||||||
|
sourceCode = sourceCode.replace(/import\.meta\.env\.MODE/g, 'process.env.MODE || "production"')
|
||||||
|
sourceCode = sourceCode.replace(/import\.meta\.env\.VITE_WEBHOOK/g, 'process.env.VITE_WEBHOOK || ""')
|
||||||
|
|
||||||
|
// 创建临时模块
|
||||||
|
const tempModulePath = path.join(__dirname, 'temp-index.js')
|
||||||
|
fs.writeFileSync(tempModulePath, sourceCode)
|
||||||
|
|
||||||
|
// 导入错误监控模块
|
||||||
|
const errorMonitor = require('./temp-index.js')
|
||||||
|
const { initErrorMonitor, reportError, getErrorStats, resetErrorStats, getErrorLevel, setErrorLevel, clearErrorCache, ERROR_LEVEL } = errorMonitor
|
||||||
|
|
||||||
|
// 测试配置
|
||||||
|
const TEST_WEBHOOK = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=9a401eb2-065a-4882-82e9-b438bcd1eac4'
|
||||||
|
|
||||||
|
// 测试结果统计
|
||||||
|
let passCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言函数
|
||||||
|
*/
|
||||||
|
function assert(condition, testName) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(`✅ 通过: ${testName}`)
|
||||||
|
passCount++
|
||||||
|
} else {
|
||||||
|
console.log(`❌ 失败: ${testName}`)
|
||||||
|
failCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟函数
|
||||||
|
*/
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试套件
|
||||||
|
*/
|
||||||
|
async function runTests() {
|
||||||
|
console.log('\n========================================')
|
||||||
|
console.log(' 错误监控功能测试')
|
||||||
|
console.log('========================================\n')
|
||||||
|
|
||||||
|
// ========== 测试1: 初始化 ==========
|
||||||
|
console.log('📋 测试组1: 初始化测试')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
// 初始化错误监控
|
||||||
|
initErrorMonitor({
|
||||||
|
webhookUrl: TEST_WEBHOOK,
|
||||||
|
forceEnable: true,
|
||||||
|
errorLevel: ERROR_LEVEL.STRICT,
|
||||||
|
dedupInterval: 5000, // 5秒去重间隔(测试用)
|
||||||
|
})
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
assert(getErrorLevel() === ERROR_LEVEL.STRICT, '初始化后错误级别应为 strict')
|
||||||
|
|
||||||
|
// ========== 测试2: 错误级别功能 ==========
|
||||||
|
console.log('\n📋 测试组2: 错误级别功能')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
// 测试设置错误级别
|
||||||
|
setErrorLevel(ERROR_LEVEL.STANDARD)
|
||||||
|
assert(getErrorLevel() === ERROR_LEVEL.STANDARD, '设置错误级别应为 standard')
|
||||||
|
|
||||||
|
setErrorLevel(ERROR_LEVEL.SILENT)
|
||||||
|
assert(getErrorLevel() === ERROR_LEVEL.SILENT, '设置错误级别应为 silent')
|
||||||
|
|
||||||
|
// 恢复为 strict 进行后续测试
|
||||||
|
setErrorLevel(ERROR_LEVEL.STRICT)
|
||||||
|
|
||||||
|
// ========== 测试3: 手动上报错误 ==========
|
||||||
|
console.log('\n📋 测试组3: 手动上报错误')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
const stats1 = getErrorStats()
|
||||||
|
assert(stats1.total === 0, '重置后错误总数应为0')
|
||||||
|
|
||||||
|
// 上报一个手动错误
|
||||||
|
reportError('manual', new Error('测试手动错误'), { testId: 'test-001' })
|
||||||
|
|
||||||
|
await delay(200)
|
||||||
|
const stats2 = getErrorStats()
|
||||||
|
assert(stats2.total >= 1, '上报后错误总数应增加')
|
||||||
|
assert(stats2.manual >= 1, '手动错误计数应增加')
|
||||||
|
|
||||||
|
// ========== 测试4: 错误去重功能 ==========
|
||||||
|
console.log('\n📋 测试组4: 错误去重功能')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
// 上报相同错误两次
|
||||||
|
const testError = new Error('重复错误测试')
|
||||||
|
reportError('manual', testError, { dedupTest: 1 })
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
const stats3 = getErrorStats()
|
||||||
|
const firstCount = stats3.total
|
||||||
|
|
||||||
|
// 立即再次上报相同错误(应该被去重)
|
||||||
|
reportError('manual', testError, { dedupTest: 1 })
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
const stats4 = getErrorStats()
|
||||||
|
assert(stats4.total === firstCount, '相同错误在去重间隔内不应重复上报')
|
||||||
|
|
||||||
|
// 清空缓存后再次上报
|
||||||
|
clearErrorCache()
|
||||||
|
reportError('manual', testError, { dedupTest: 1 })
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
const stats5 = getErrorStats()
|
||||||
|
assert(stats5.total > firstCount, '清空缓存后可以重新上报')
|
||||||
|
|
||||||
|
// ========== 测试5: API错误上报 ==========
|
||||||
|
console.log('\n📋 测试组5: API错误上报')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
// 模拟API错误响应
|
||||||
|
const apiError = {
|
||||||
|
config: {
|
||||||
|
url: 'https://api.example.com/test',
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: 123 },
|
||||||
|
header: { 'Content-Type': 'application/json' },
|
||||||
|
startTime: Date.now() - 500,
|
||||||
|
},
|
||||||
|
statusCode: 500,
|
||||||
|
data: {
|
||||||
|
code: 500,
|
||||||
|
msg: '服务器内部错误',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError('api', apiError)
|
||||||
|
|
||||||
|
await delay(200)
|
||||||
|
const stats6 = getErrorStats()
|
||||||
|
assert(stats6.api >= 1, 'API错误应被正确统计')
|
||||||
|
|
||||||
|
// ========== 测试6: 网络错误上报 ==========
|
||||||
|
console.log('\n📋 测试组6: 网络错误上报')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
reportError('network', new Error('网络连接失败'), {
|
||||||
|
url: 'https://api.example.com/network-test',
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
await delay(200)
|
||||||
|
const stats7 = getErrorStats()
|
||||||
|
assert(stats7.network >= 1, '网络错误应被正确统计')
|
||||||
|
|
||||||
|
// ========== 测试7: 强制发送 ==========
|
||||||
|
console.log('\n📋 测试组7: 强制发送功能')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
clearErrorCache()
|
||||||
|
|
||||||
|
setErrorLevel(ERROR_LEVEL.SILENT) // 设置为静默模式
|
||||||
|
|
||||||
|
// 在静默模式下,普通错误不应上报
|
||||||
|
reportError('promise', new Error('静默模式Promise错误'))
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
const stats8 = getErrorStats()
|
||||||
|
const silentCount = stats8.total
|
||||||
|
|
||||||
|
// 使用强制发送
|
||||||
|
reportError('promise', new Error('强制发送的Promise错误'), {}, true)
|
||||||
|
|
||||||
|
await delay(200)
|
||||||
|
const stats9 = getErrorStats()
|
||||||
|
assert(stats9.total > silentCount, '强制发送应绕过错误级别过滤')
|
||||||
|
|
||||||
|
// 恢复为 strict
|
||||||
|
setErrorLevel(ERROR_LEVEL.STRICT)
|
||||||
|
|
||||||
|
// ========== 测试8: 错误统计重置 ==========
|
||||||
|
console.log('\n📋 测试组8: 错误统计重置')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
|
||||||
|
reportError('manual', new Error('测试错误'))
|
||||||
|
|
||||||
|
await delay(100)
|
||||||
|
const beforeReset = getErrorStats()
|
||||||
|
assert(beforeReset.total > 0, '重置前应有错误记录')
|
||||||
|
|
||||||
|
resetErrorStats()
|
||||||
|
const afterReset = getErrorStats()
|
||||||
|
assert(afterReset.total === 0, '重置后错误总数应为0')
|
||||||
|
assert(afterReset.global === 0, '重置后全局错误数应为0')
|
||||||
|
assert(afterReset.promise === 0, '重置后Promise错误数应为0')
|
||||||
|
|
||||||
|
// ========== 测试总结 ==========
|
||||||
|
console.log('\n========================================')
|
||||||
|
console.log(' 测试总结')
|
||||||
|
console.log('========================================')
|
||||||
|
console.log(`✅ 通过: ${passCount}`)
|
||||||
|
console.log(`❌ 失败: ${failCount}`)
|
||||||
|
console.log(`📊 总计: ${passCount + failCount}`)
|
||||||
|
console.log('========================================\n')
|
||||||
|
|
||||||
|
if (failCount === 0) {
|
||||||
|
console.log('🎉 所有测试通过!')
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 部分测试失败,请检查相关功能')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tempModulePath)
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略清理错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return failCount === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
runTests().catch(console.error)
|
||||||
Reference in New Issue
Block a user