前端错误日志收集

前端错误日志收集

1、采集内容

  • 用户信息:当前状态、权限 等
  • 行为信息:界面路径、执行的操作 等
  • 异常信息:错误栈、错误描述、配置的信息 等
  • 环境信息:设备型号、标识、操作系统 等
字段类型描述
timeString错误发生的时间
userIdString用户id
userNameString用户名
userRolesArray用户权限列表
errorStackString错误栈
errorTimeStampNumber时间戳
UAString浏览器 userAgent
configObject请求接口失败时,配置信息
requestObject请求接口失败时,响应信息/请求信息

2、异常捕获

2.1 全局捕获 window.onerror

重写 onerror、或者监听 onerror 事件,全局收集错误信息

window.onerror = function (errorType, errorFilename, errorLineNo, errorColNo, error) {
 // errorType 		=> 错误类型
 // errorFilename 	=> 错误文件名
 // errorLineNo 		=> 错误行
 // errorColNo 		=> 错误列
 // error 			=> 错误信息,error.message 错误描述,error.stack 错误栈
 const errorInfo = {
     errorMessage: `[Window warn]: ${error.message}${error.stack}`,
     errorStack: error.stack,
     ...
 }
}

2.2 全局捕获 window.unhandledrejection

通过监听 unhandledrejection 发出的错误,收集 Promise 抛出的异常错误信息;

window.addEventListener('unhandledrejection', error => {
    const saveError = sysInfo.getData({
        errorMessage: `[Window warn]: ${error.reason.message}${error.reason.stack}`,
        errorStack: error.reason.stack,
        ...
    })
})

2.3 Vue.config.errorHandler 捕获错误

Vue debug.js 解析错误栈

利用 Vue 提供 debug 能力,收集错误信息;通过 Vue 源码中提供的 debug.js 找到错误栈

// https://github.com/vuejs/vue/blob/master/src/core/util/debug.js
const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str) => str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
const ROOT_COMPONENT_NAME = '<Root>'
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>'
const repeat = (str, n) => {
    let res = ''
    while (n) {
          if (n % 2 === 1) res += str
          if (n > 1) str += str // eslint-disable-line no-param-reassign
           n >>= 1 // eslint-disable-line no-bitwise, no-param-reassign
       }
   	return res
   }
   export const formatComponentName = (vm, includeFile) => {
       if (!vm) return ANONYMOUS_COMPONENT_NAME
       if (vm.$root === vm) return ROOT_COMPONENT_NAME
  	const options = vm.$options
  	let name = options.name || options._componentTag
    const file = options.__file
    if (!name && file) {
        const match = file.match(/([^/\\]+)\.vue$/)
          if (match) name = match[1]
       }
      return (name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : ``)
}
  export const generateComponentTrace = (vm) => {
       if (vm?._isVue && vm?.$parent) {
          const tree = []
    	let currentRecursiveSequence = 0
          while (vm) {
            if (tree.length > 0) {
                  const last = tree[tree.length - 1]
                  if (last.constructor === vm.constructor) {
                      currentRecursiveSequence += 1
                       vm = vm.$parent // eslint-disable-line no-param-reassign
                       continue
                   } else if (currentRecursiveSequence > 0) {
                       tree[tree.length - 1] = [last, currentRecursiveSequence]
                      currentRecursiveSequence = 0
                }
              }
               tree.push(vm)
              vm = vm.$parent // eslint-disable-line no-param-reassign
        }
        const formattedTree = tree
        .map((vm, i) =>
              `${(i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) +
               (Array.isArray(vm)
                ? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
                : formatComponentName(vm))}`)
          	.join('\n')
       	return `\n\nfound in\n\n${formattedTree}`
   	}
   	return `\n\n(found in ${formatComponentName(vm)})`
   }
   
Vue.config.errorHandler 收集错误信息

options 配置信息

{
	attachProps: '是否获取属性',
 	logErrors: '是否提示错误',
    ...
}
const { errorHandler, silent } = Vue.config
Vue.config.errorHandler = (error, vm, lifecycleHook) => {
    // 解析错误栈
	const trace = vm ? generateComponentTrace(vm) : ''
    const message = `Error in ${lifecycleHook}: "${error && error.toString()}"`
    // 是否已经存在有一个 errorHandler,如果存在 则混入错误信息
    if (typeof errorHandler === 'function') {
        errorHandler.call(Vue, error, vm, lifecycleHook)
    }
   // 在控制台输出 错误日志 
    if (options.logErrors) {
        const hasConsole = typeof console !== 'undefined'
        if (Vue.config.warnHandler) {
            Vue.config.warnHandler.call(null, message, vm, trace)
        } else if (hasConsole && !silent) {
            // eslint-disable-next-line no-console
            console.error(`[Vue warn]: ${message}${trace}`)
        }
    }
    const errorInfo = {
        errorMessage: `[Vue warn]: ${message}${trace}`,
        errorStack: trace,
        ...
    }
}

2.4 收集 request

  • 在 axios 拦截器中,收集并解析错误信息
const interceptorReject = err => {
    requestError(error)
}
axios.interceptors.request.use(e => {}, interceptorReject)
axios.interceptors.response.use(e => {}, interceptorReject)
  • 解析错误日志,获取 请求的配置信息、请求头、响应信息等
requestError (error) {
    try {
        const {
            config: { data, headers: ConfigHeaders, maxContentLength, method, timeout, url, xsrfCookieName, xsrfHeaderName },
            status,
            headers,
            request: { readyState, response, responseText, responseType, responseURL, responseXML, status: requestStatus, statusText: requestStatusText, withCredentials },
            statusText
        } = { ...error.response }
        const errorInfo = {
            requests: {
                config: {
                    data, headers: ConfigHeaders, maxContentLength, method, timeout, url, xsrfCookieName, xsrfHeaderName
                },
                status,
                headers: { ...headers },
                request: {
                    readyState,
                    response,
                    responseText,
                    responseType,
                    responseURL,
                    responseXML,
                    status: requestStatus,
                    statusText: requestStatusText,
                    withCredentials
                },
                statusText
            },
            errorMessage: `[Request warn]: ${error.message}${error.stack}`,
            errorStack: error.stack,
            ...
        })
    } catch (err) { ... }
}

3、日志存储

选择 localStorage OR IndexedDB ?

  • localStorage 存储空间 大概在 2 ~ 10M,可以长期保留,API 使用简单,但存储类型有一定的限制,且 读写都是同步进行的,容易阻塞线程
  • IndexedDB 是一个事务型数据库系统;存储空间一般没有限制,可以长期保留,用于在客户端存储大量的结构化数据,API 同时提供了索引实现对数据的高性能搜索,读写异步进行,不易阻塞线程,API 是强大的,但对于简单的情况可能看起来太复杂;

程序错误信息会不间断的收集、为了方便统计和存储大量的错误信息,IndexedDB 显得更加的适应需求;

IndexedDB API 相对比较复杂,因此需要对其进行封装,使其使用起来更加的便捷;

封装 IndexedDB

  • 根据 IndexedDB API 进行封装

收集到的日志可以 添加到浏览器 IndexedDB 数据库中;

const db = new Database()
...
db.add({	// 可以将收集到的错误信息(errorInfo),通过此方法添加到浏览器数据库中
    userId: '',
    time: '',
    ...errorInfo,
    ...
})
/* 200  => 获取成功
*  404  => 未找到数据
*  500  => 操作失败
*  1004 => 浏览器不支持 IndexedDB
*/
const _log = function (...e) {
    // console.log('%c[[IDB]]: ', 'color: #00f;line-height: 24px;', ...e)
}
const isIDB = 'indexedDB' in window
class Database {
    // 初始化
    constructor (db = 'JSErrorDB', sn = 'JSErrorStore', v = 1) {
        this.IDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB
        this.db = null
        this.store = null
        this.IDBName = db
        this.storeName = sn
        this.version = v
        if (isIDB) this.init()
        else console.log('%c 初始化 IndexedDB 失败,浏览器暂不支持!')
    }
    init () {
        const openRequest = this.IDB.open(this.IDBName, this.version)
        // 版本更新时 重新连接仓库
        openRequest.onupgradeneeded = e => {
            this.db = e.target.result
            this.createdStore()
        }
		// 连接数据库成功后,连接仓库
        openRequest.onsuccess = e => {
            this.db = e.target.result
            this.createdStore()
        }
		// 连接数据库失败,可以尝试重新连接 this.init()
        openRequest.onerror = e => { ... }
    }
	// 创建仓库
    createdStore () {
        if (!this.db.objectStoreNames.contains(this.storeName)) {
            this.db.createObjectStore(this.storeName, { autoIncrement: true })
        }
    }
	// 创建事务
    transaction () {
        const t = this.db.transaction([this.storeName], 'readwrite')
        t.onabort = e => { _log('事务中断', e) }
        t.oncomplete = e => { _log('事务已完成', e) }
        t.onerror = e => { _log('事务出错', e) }
        const store = t.objectStore(this.storeName)
        return store
    }
	// 添加数据
    add (v) {
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            _log('执行函数', this.db)
            const store = this.transaction()
            const request = store.add(v)
            request.onsuccess = e => {
                _log('添加数据成功', e.target.result, v)
                resolve({
                    code: 200,
                    data: {
                        key: e.target.result,
                        data: v
                    },
                    message: '添加成功'
                })
            }
            request.onerror = e => {
                _log('添加数据失败', e)
                reject(e)
            }
        })
    }
	// 根据 Key 获取详情
    get (n) {
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const ob = store.get(n)
            ob.onsuccess = e => {
                _log(`读取[[${n}]]成功`, e)
                resolve({
                    code: e.target.result ? 200 : 404,
                    data: e.target.result,
                    message: e.target.result ? '获取成功' : '未找到对应 key'
                })
            }
            ob.onerror = e => {
                _log(`读取[[${n}]]失败`, e)
                reject(e)
            }
        })
    }
	// 获取列表
    getList (obj = {}) {
        const { page = 1, size = 50 } = obj
        let advanced = false
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const request = store.openCursor()
            const data = []
            request.onsuccess = e => {
                const cursor = e.target.result
                _log('获取所有信息', e, data.length < (page * size), page, size)
                if (cursor && cursor !== null && data.length < size) {
                    if (!advanced && page !== 1) {
                        advanced = true
                        cursor.advance((page - 1) * size)
                        return
                    }
                    data.push({
                        key: cursor.key,
                        data: cursor.value
                    })
                    cursor.continue()
                } else {
                    _log('遍历结束', e, data)
                    resolve({
                        code: 200,
                        data,
                        message: '获取成功'
                    })
                }
            }
            request.onerror = e => {
                _log('遍历错误:', e)
                reject(e)
            }
        })
    }
	// 更新信息
    put (obj = {}) {
        const { key, data } = obj
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const request = store.put(data, key)
            request.onsuccess = e => {
                _log('更新数据成功', data, e)
                resolve({
                    code: 200,
                    data: {
                        key: key,
                        data: data
                    },
                    message: '更新成功'
                })
            }
            request.onerror = e => {
                _log('更新数据失败', e)
                reject(e)
            }
        })
    }
	// 删除指定信息
    delete (k) {
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const request = store.delete(k)
            request.onsuccess = e => {
                _log('删除数据成功', k, e)
                resolve({
                    code: 200,
                    data: e.target.result,
                    message: '删除成功'
                })
            }
            request.onerror = e => {
                _log('删除数据失败', e)
                reject(e)
            }
        })
    }
	// 默认清理一个月之前的日志
    expireClear (time = 60 * 60 * 24 * 30 * 1000) {
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const request = store.openCursor()
            const data = []
            const currentTime = new Date().getTime()
            request.onsuccess = e => {
                const cursor = e.target.result
                if (cursor && cursor !== null && currentTime - time > cursor.value.errorTimeStamp) {
                    data.push({
                        key: cursor.key,
                        data: cursor.value
                    })
                    cursor.delete()
                    cursor.continue()
                } else {
                    resolve({
                        code: 200,
                        data,
                        message: '清理结束'
                    })
                }
            }
            request.onerror = e => {
                _log('遍历错误:', e)
                reject(e)
            }
        })
    }
	// 清理所有日志
    clear () {
        return new Promise((resolve, reject) => {
            if (!isIDB) return reject({
                code: 1004,
                message: '浏览器暂不支持 IndexedDB!'
            })
            const store = this.transaction()
            const request = store.clear()
            request.onsuccess = e => {
                resolve({
                    code: 200,
                    data: e.target.result,
                    message: '清空数据成功'
                })
            }
            request.onerror = e => {
                _log('清空数据失败', e)
                reject(e)
            }
        })
    }
	// 关闭连接
    close () {
        this.db.close()
    }
}
export {
	Database
}

日志查看

  • 通过连接浏览器仓库查看日志记录

在这里插入图片描述

<template>
    <div class='log'>
        <div class='flex btn'>
            <button @click='$router.go(-1)'> 返回 </button>
            <input type="text" v-model='page' placeholder='页'>
            <input type="text" v-model='size' placeholder='条'>
            <button @click='btn("getList")'> 获取第{{page}}页数据 </button>
            <button @click='btn("clear")'> 删除全部 </button>
            <button @click='btn("getErrorInfoList")'> 获取错误日志 </button>
        </div>
        <div>
            <div class='log-item' v-for='(v, i) in errorInfoList' :key=i>
                <div class='flex-left'>
                    <div>{{v.data.userName}}</div>
                    <div>{{v.data.errorTime}}</div>
                    <div>{{v.data.errorType}}</div>
                </div>
                <div class='flex-right'>
                    <div class='log-request' v-if='v.data.errorType === "Request"'>
                        {{v.data.requests}}
    				</div>
                    <div>{{v.data.errorMessage || v.data.errorStack}}</div>
                </div>
            </div>
    	</div>
    </div>
</template>
<script>
    import { Database } from '@/utils/JSError/indexedDB'
    const a = 13
    export default {
        name: 'ErrorLog',
        data () {
            return {
                errorInfoList: [],
                page: 1,
                size: 20
            }
        },
        async created () {
            this.db = new Database()
        },
        methods: {
            btn (type) {
                const { page, size } = this
                if (type === 'getList') this.db.getList({
                    page: page,
                    size: size
                }).then(res => {
                    if (res.code === 200) {
                        res.data.forEach(v => {
                            console.error(v.data.errorTime, '\n', v.data.errorMessage)
                        })
                        this.errorInfoList = res.data
                    }
                })
                if (type === 'clear') this.db.clear().then(res => {
                    console.log('清空文本成功', res)
                })
                if (type === 'getErrorInfoList') this.getErrorInfoList()
            },
            getErrorInfoList () {
                this.db.getList().then(res => {
                    console.log('获取错误日志', res)
                    if (res.code === 200) {
                        res.data.forEach(v => {
                            console.error(v.data.errorTime, '\n', v.data.errorMessage)
                        })
                        this.errorInfoList = res.data
                    }
                })
            }
        }
    }
</script>

4、后期完善

4.1 整理日志

4.2 上报日志

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值