Front end error log collection

Posted by bailo81 on Sun, 02 Jan 2022 08:56:52 +0100

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
fieldtypedescribe
timeStringWhen the error occurred
userIdStringUser id
userNameStringuser name
userRolesArrayUser permission list
errorStackStringError stack
errorTimeStampNumbertime stamp
UAStringBrowser userAgent
configObjectConfiguration information when the request interface fails
requestObjectResponse 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>

4. Improvement of later plan

4.1 sorting logs

4.2 reporting log

Topics: Javascript Front-end Vue.js