You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
7.0 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 全局错误上报模块
* 用于捕获和上报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
}