You've already forked template-MP
新增 全局JS错误监控模块,集成WebHook上报功能
This commit is contained in:
@@ -16,6 +16,19 @@ class Tool {
|
||||
|
||||
// 字体加载状态缓存
|
||||
this.loadedFonts = new Set()
|
||||
|
||||
// 初始化错误统计
|
||||
this.errorStats = {
|
||||
total: 0,
|
||||
global: 0,
|
||||
promise: 0,
|
||||
console: 0,
|
||||
miniProgram: 0,
|
||||
lastErrorTime: null,
|
||||
}
|
||||
|
||||
// Promise包装方法
|
||||
this.wrapPromise = null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,6 +370,497 @@ class Tool {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局错误监控
|
||||
* @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] 重试延迟时间(毫秒)
|
||||
*/
|
||||
initErrorMonitor(options = {}) {
|
||||
const config = {
|
||||
enableGlobalError: true,
|
||||
enablePromiseError: true,
|
||||
enableConsoleError: false,
|
||||
webhookUrl: import.meta.env.VITE_WEBHOOK,
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
...options,
|
||||
}
|
||||
|
||||
// 检查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 {Error|Object} error 错误对象或错误信息
|
||||
* @param {Object} [context] 错误上下文信息
|
||||
*/
|
||||
reportError(error, context = {}) {
|
||||
const errorInfo = {
|
||||
type: 'manual',
|
||||
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: getCurrentPageName(),
|
||||
}
|
||||
|
||||
this._sendErrorToWebhook(errorInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误统计信息
|
||||
* @returns {Object} 错误统计信息
|
||||
*/
|
||||
getErrorStats() {
|
||||
return { ...this.errorStats }
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置错误统计
|
||||
*/
|
||||
resetErrorStats() {
|
||||
this.errorStats = {
|
||||
total: 0,
|
||||
global: 0,
|
||||
promise: 0,
|
||||
console: 0,
|
||||
lastErrorTime: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全局错误
|
||||
* @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.miniProgram++
|
||||
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,
|
||||
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) {
|
||||
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
|
||||
*/
|
||||
_formatErrorMessage(errorInfo) {
|
||||
const timestamp = new Date(errorInfo.timestamp).toLocaleString('zh-CN')
|
||||
|
||||
let message = `🚨 JavaScript错误报告\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`
|
||||
message += `📝 错误信息: ${this._serializeError(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`
|
||||
|
||||
// 添加设备信息
|
||||
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 '未知页面'
|
||||
}
|
||||
|
||||
// 创建单例并导出
|
||||
|
||||
Reference in New Issue
Block a user