|
|
|
|
@ -0,0 +1,270 @@
|
|
|
|
|
/**
|
|
|
|
|
* 全局错误上报模块
|
|
|
|
|
* 用于捕获和上报H5前端错误
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const ERROR_REPORT_API = 'https://tj-h5.hnxdfe.com/api/H5/test'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建错误信息对象
|
|
|
|
|
*/
|
|
|
|
|
const buildErrorInfo = (error, type = 'error') => {
|
|
|
|
|
const errorInfo = {
|
|
|
|
|
type, // error: js错误, promise: promise错误, unhandled: 未捕获异常
|
|
|
|
|
message: '',
|
|
|
|
|
stack: '',
|
|
|
|
|
url: '',
|
|
|
|
|
ua: '',
|
|
|
|
|
timestamp: '',
|
|
|
|
|
openId: '',
|
|
|
|
|
rawError: null // 保留原始错误对象(用于调试)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
errorInfo.timestamp = new Date().toISOString()
|
|
|
|
|
errorInfo.url = window.location.href
|
|
|
|
|
errorInfo.ua = navigator.userAgent
|
|
|
|
|
|
|
|
|
|
// 获取用户信息
|
|
|
|
|
try {
|
|
|
|
|
errorInfo.openId = uni.getStorageSync('OPENID') || ''
|
|
|
|
|
} catch (e) {
|
|
|
|
|
errorInfo.openId = '获取失败'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理不同类型的错误
|
|
|
|
|
if (error instanceof Error) {
|
|
|
|
|
errorInfo.message = error.message || String(error)
|
|
|
|
|
errorInfo.stack = error.stack || ''
|
|
|
|
|
} else if (typeof error === 'string') {
|
|
|
|
|
errorInfo.message = error
|
|
|
|
|
errorInfo.stack = ''
|
|
|
|
|
} else if (error && typeof error === 'object') {
|
|
|
|
|
try {
|
|
|
|
|
// 检查 message 是否是 "{}" 或空字符串
|
|
|
|
|
const msg = error.message || ''
|
|
|
|
|
|
|
|
|
|
if (msg === '{}' || msg === '' || msg === 'undefined' || msg === 'null') {
|
|
|
|
|
// 尝试从其他字段获取信息
|
|
|
|
|
if (error.reason) {
|
|
|
|
|
errorInfo.message = String(error.reason)
|
|
|
|
|
} else if (error.name) {
|
|
|
|
|
errorInfo.message = error.name
|
|
|
|
|
} else if (error.toString && typeof error.toString === 'function') {
|
|
|
|
|
const str = error.toString()
|
|
|
|
|
if (str && str !== '[object Object]') {
|
|
|
|
|
errorInfo.message = str
|
|
|
|
|
} else {
|
|
|
|
|
errorInfo.message = '未知错误对象(无有效信息)'
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 尝试序列化对象
|
|
|
|
|
const jsonStr = JSON.stringify(error)
|
|
|
|
|
if (jsonStr && jsonStr !== '{}') {
|
|
|
|
|
errorInfo.message = jsonStr
|
|
|
|
|
} else {
|
|
|
|
|
errorInfo.message = '错误对象为空'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
errorInfo.message = msg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
errorInfo.stack = error.stack || ''
|
|
|
|
|
|
|
|
|
|
// 如果没有 stack,尝试构建堆栈信息
|
|
|
|
|
if (!errorInfo.stack && error.filename) {
|
|
|
|
|
errorInfo.stack = `at ${error.filename}:${error.lineno || 0}:${error.colno || 0}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保留原始错误对象的前几个键(用于调试)
|
|
|
|
|
const keys = Object.keys(error).slice(0, 5)
|
|
|
|
|
if (keys.length > 0) {
|
|
|
|
|
errorInfo.rawError = {}
|
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
const key = keys[i]
|
|
|
|
|
if (typeof error[key] !== 'function') {
|
|
|
|
|
errorInfo.rawError[key] = String(error[key]).substring(0, 100)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
errorInfo.message = '解析错误对象失败: ' + String(e)
|
|
|
|
|
errorInfo.stack = ''
|
|
|
|
|
}
|
|
|
|
|
} else if (error === null || error === undefined) {
|
|
|
|
|
errorInfo.message = '错误对象为 ' + String(error)
|
|
|
|
|
errorInfo.stack = ''
|
|
|
|
|
} else {
|
|
|
|
|
errorInfo.message = String(error) || '未知错误'
|
|
|
|
|
errorInfo.stack = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果 message 还是空的或 {}
|
|
|
|
|
if (!errorInfo.message || errorInfo.message === '{}' || errorInfo.message === 'undefined') {
|
|
|
|
|
errorInfo.message = '无法获取错误详情(错误对象为空或无效)'
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[错误上报] 构建错误信息失败', e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return errorInfo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用 sendBeacon 上报(推荐)
|
|
|
|
|
* 优势:即使页面关闭也能发送,不阻塞页面
|
|
|
|
|
*/
|
|
|
|
|
const reportByBeacon = (errorInfo) => {
|
|
|
|
|
try {
|
|
|
|
|
if (navigator.sendBeacon) {
|
|
|
|
|
const data = JSON.stringify(errorInfo)
|
|
|
|
|
const blob = new Blob([data], { type: 'application/json' })
|
|
|
|
|
return navigator.sendBeacon(ERROR_REPORT_API, blob)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[错误上报] sendBeacon 失败', e)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用 fetch 上报(备用方案)
|
|
|
|
|
*/
|
|
|
|
|
const reportByFetch = (errorInfo) => {
|
|
|
|
|
try {
|
|
|
|
|
fetch(ERROR_REPORT_API, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(errorInfo),
|
|
|
|
|
keepalive: true // 即使页面关闭也保持连接
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.warn('[错误上报] fetch 失败', err)
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[错误上报] fetch 执行失败', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用 uni.request 上报(最后备用)
|
|
|
|
|
*/
|
|
|
|
|
const reportByUni = (errorInfo) => {
|
|
|
|
|
try {
|
|
|
|
|
uni.request({
|
|
|
|
|
url: ERROR_REPORT_API,
|
|
|
|
|
method: 'POST',
|
|
|
|
|
data: errorInfo,
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.warn('[错误上报] uni.request 失败', err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[错误上报] uni.request 执行失败', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断是否应该忽略的错误
|
|
|
|
|
*/
|
|
|
|
|
const shouldIgnoreError = (error) => {
|
|
|
|
|
// 忽略请求中止错误(页面跳转时常见)
|
|
|
|
|
if (error && error.errMsg === 'request:fail abort') {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if (error && error.message && error.message.includes('abort')) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
// 忽略取消的错误
|
|
|
|
|
if (error && error.name === 'AbortError') {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 上报错误到服务器
|
|
|
|
|
*/
|
|
|
|
|
const reportError = (error, type = 'error') => {
|
|
|
|
|
try {
|
|
|
|
|
// 检查是否应该忽略此错误
|
|
|
|
|
if (shouldIgnoreError(error)) {
|
|
|
|
|
console.warn('[错误上报] 忽略请求中止错误', error)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errorInfo = buildErrorInfo(error, type)
|
|
|
|
|
|
|
|
|
|
// 打印到控制台
|
|
|
|
|
console.error('[错误上报]', errorInfo)
|
|
|
|
|
|
|
|
|
|
// 优先使用 sendBeacon(最可靠)
|
|
|
|
|
const beaconSuccess = reportByBeacon(errorInfo)
|
|
|
|
|
if (beaconSuccess) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 备用:使用 fetch
|
|
|
|
|
reportByFetch(errorInfo)
|
|
|
|
|
|
|
|
|
|
// 兜底:使用 uni.request(异步,可能失败)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
reportByUni(errorInfo)
|
|
|
|
|
}, 100)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[错误上报] 上报失败', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化全局错误捕获
|
|
|
|
|
*/
|
|
|
|
|
const initErrorHandler = () => {
|
|
|
|
|
console.log('[错误上报] 初始化错误捕获')
|
|
|
|
|
|
|
|
|
|
// 1. 捕获 JavaScript 运行时错误
|
|
|
|
|
window.addEventListener('error', (event) => {
|
|
|
|
|
if (event.error) {
|
|
|
|
|
reportError(event.error, 'error')
|
|
|
|
|
} else {
|
|
|
|
|
reportError({
|
|
|
|
|
message: event.message,
|
|
|
|
|
filename: event.filename,
|
|
|
|
|
lineno: event.lineno,
|
|
|
|
|
colno: event.colno
|
|
|
|
|
}, 'error')
|
|
|
|
|
}
|
|
|
|
|
}, true)
|
|
|
|
|
|
|
|
|
|
// 2. 捕获 Promise 未处理的 rejection 错误
|
|
|
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
|
|
|
reportError({
|
|
|
|
|
message: 'Promise rejection',
|
|
|
|
|
reason: event.reason
|
|
|
|
|
}, 'promise')
|
|
|
|
|
// 阻止默认的控制台警告
|
|
|
|
|
// event.preventDefault()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 3. 捕获 Vue 错误(需要在 App.vue 中使用 app.config.errorHandler)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 手动上报错误
|
|
|
|
|
* 用于主动上报一些业务错误
|
|
|
|
|
*/
|
|
|
|
|
const report = (message, data = {}) => {
|
|
|
|
|
const error = {
|
|
|
|
|
message,
|
|
|
|
|
...data
|
|
|
|
|
}
|
|
|
|
|
reportError(error, 'manual')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
initErrorHandler,
|
|
|
|
|
report,
|
|
|
|
|
reportError
|
|
|
|
|
}
|