Front end error log collection
1. Collection content
- User information: current status, permissions, etc
- Behavior information: interface path, executed operation, etc
- Exception information: error stack, error description, configuration information, etc
- Environmental information: equipment model, identification, operating system, etc
field | type | describe |
---|---|---|
time | String | When the error occurred |
userId | String | User id |
userName | String | user name |
userRoles | Array | User permission list |
errorStack | String | Error stack |
errorTimeStamp | Number | time stamp |
UA | String | Browser userAgent |
config | Object | Configuration information when the request interface fails |
request | Object | Response information / request information when the request interface fails |
... | ... | ... |
2. Exception capture
2.1 global capture window onerror
Rewrite oneror or listen to oneror events to collect error information globally
window.onerror = function (errorType, errorFilename, errorLineNo, errorColNo, error) { // errorType => Error type // errorFilename => Bad file name // errorLineNo => Wrong line // errorColNo => Error column // error => Error message, error Message error description, error Stack error stack const errorInfo = { errorMessage: `[Window warn]: ${error.message}${error.stack}`, errorStack: error.stack, ... } }
2.2 Vue.config.errorHandler caught an error
Vue debug.js parsing error stack
Use Vue to provide debug capability and collect error information; Through the software provided in Vue source code debug.js Error stack found
// 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 collects error information
options configuration information
{ attachProps: 'Get properties', logErrors: 'Prompt error', ... }
const { errorHandler, silent } = Vue.config Vue.config.errorHandler = (error, vm, lifecycleHook) => { // Parse error stack const trace = vm ? generateComponentTrace(vm) : '' const message = `Error in ${lifecycleHook}: "${error && error.toString()}"` // Whether an errorHandler already exists. If so, the error information will be mixed in if (typeof errorHandler === 'function') { errorHandler.call(Vue, error, vm, lifecycleHook) } // Output error log in console 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.3 collect request
- In the axios interceptor, collect and parse error information
const interceptorReject = err => { requestError(error) } axios.interceptors.request.use(e => {}, interceptorReject) axios.interceptors.response.use(e => {}, interceptorReject)
- Parse the error log to obtain the request configuration information, request header, response information, etc
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. Log storage
Select localStorage OR IndexedDB?
- The storage space of localStorage is about 2 ~ 10M, which can be reserved for a long time. The API is simple to use, but the storage type has certain restrictions, and the reading and writing are synchronous, which is easy to block threads
- IndexedDB is a transactional database system; The storage space is generally unlimited and can be reserved for a long time. It is used to store a large amount of structured data on the client. The API also provides an index to realize high-performance data search. Reading and writing are asynchronous and it is not easy to block threads. The API is powerful, but it may seem too complex for simple situations;
Program error information will be collected continuously. In order to facilitate the statistics and storage of a large number of error information, IndexedDB seems to be more adaptable to the needs;
IndexedDB API is relatively complex, so it needs to be encapsulated to make it more convenient to use;
Encapsulating IndexedDB
- Encapsulated according to IndexedDB API
The collected logs can be added to the IndexedDB database of the browser;
const db = new Database() ... db.add({ // You can add the collected error information (errorInfo) to the browser database through this method userId: '', time: '', ...errorInfo, ... })
/* 200 => Get success * 404 => No data found * 500 => operation failed * 1004 => The browser does not support IndexedDB */ const _log = function (...e) { // console.log('%c[[IDB]]: ', 'color: #00f;line-height: 24px;', ...e) } const isIDB = 'indexedDB' in window class Database { // initialization 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 initialization IndexedDB Failed, the browser does not support it!') } init () { const openRequest = this.IDB.open(this.IDBName, this.version) // Reconnect the warehouse when the version is updated openRequest.onupgradeneeded = e => { this.db = e.target.result this.createdStore() } // After successfully connecting to the database, connect to the warehouse openRequest.onsuccess = e => { this.db = e.target.result this.createdStore() } // Failed to connect to the database. You can try to reconnect to this init() openRequest.onerror = e => { ... } } // Create warehouse createdStore () { if (!this.db.objectStoreNames.contains(this.storeName)) { this.db.createObjectStore(this.storeName, { autoIncrement: true }) } } // Create transaction transaction () { const t = this.db.transaction([this.storeName], 'readwrite') t.onabort = e => { _log('Transaction interruption', e) } t.oncomplete = e => { _log('Transaction completed', e) } t.onerror = e => { _log('Transaction error', e) } const store = t.objectStore(this.storeName) return store } // Add data add (v) { return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) _log('Execution function', this.db) const store = this.transaction() const request = store.add(v) request.onsuccess = e => { _log('Data added successfully', e.target.result, v) resolve({ code: 200, data: { key: e.target.result, data: v }, message: 'Added successfully' }) } request.onerror = e => { _log('Failed to add data', e) reject(e) } }) } // Get details according to Key get (n) { return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) const store = this.transaction() const ob = store.get(n) ob.onsuccess = e => { _log(`read[[${n}]]success`, e) resolve({ code: e.target.result ? 200 : 404, data: e.target.result, message: e.target.result ? 'Get success' : 'No correspondence found key' }) } ob.onerror = e => { _log(`read[[${n}]]fail`, e) reject(e) } }) } // Get list getList (obj = {}) { const { page = 1, size = 50 } = obj let advanced = false return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) const store = this.transaction() const request = store.openCursor() const data = [] request.onsuccess = e => { const cursor = e.target.result _log('Get all information', 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('End of traversal', e, data) resolve({ code: 200, data, message: 'Get success' }) } } request.onerror = e => { _log('Traversal error:', e) reject(e) } }) } // Update information put (obj = {}) { const { key, data } = obj return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) const store = this.transaction() const request = store.put(data, key) request.onsuccess = e => { _log('Data update succeeded', data, e) resolve({ code: 200, data: { key: key, data: data }, message: 'Update succeeded' }) } request.onerror = e => { _log('Failed to update data', e) reject(e) } }) } // Delete specified information delete (k) { return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) const store = this.transaction() const request = store.delete(k) request.onsuccess = e => { _log('Data deleted successfully', k, e) resolve({ code: 200, data: e.target.result, message: 'Delete succeeded' }) } request.onerror = e => { _log('Failed to delete data', e) reject(e) } }) } // The default is to clean up logs one month ago expireClear (time = 60 * 60 * 24 * 30 * 1000) { return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support 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: 'Liquidation' }) } } request.onerror = e => { _log('Traversal error:', e) reject(e) } }) } // Clean up all logs clear () { return new Promise((resolve, reject) => { if (!isIDB) return reject({ code: 1004, message: 'Browser does not support IndexedDB!' }) const store = this.transaction() const request = store.clear() request.onsuccess = e => { resolve({ code: 200, data: e.target.result, message: 'Empty data successfully' }) } request.onerror = e => { _log('Clearing data failed', e) reject(e) } }) } // Close connection close () { this.db.close() } } export { Database }
Log view
- View log records by connecting to the warehouse browser
<template> <div class='log'> <div class='flex btn'> <button @click='$router.go(-1)'> return </button> <input type="text" v-model='page' placeholder='page'> <input type="text" v-model='size' placeholder='strip'> <button @click='btn("getList")'> Get page{{page}}Page data </button> <button @click='btn("clear")'> Delete all </button> <button @click='btn("getErrorInfoList")'> Get error log </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('Empty text successfully', res) }) if (type === 'getErrorInfoList') this.getErrorInfoList() }, getErrorInfoList () { this.db.getList().then(res => { console.log('Get error log', res) if (res.code === 200) { res.data.forEach(v => { console.error(v.data.errorTime, '\n', v.data.errorMessage) }) this.errorInfoList = res.data } }) } } } </script>