├── LICENSE ├── README.md ├── _config.yml ├── example └── index.html ├── package.json └── src ├── error.js └── error.min.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ecitlm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-log-report 2 | 3 | > 前端错误日志采集上报、上报给后端分析错误日、主要用于移动端各手机类型错误日志的收集分析 4 | 5 | ### 为何要做错误日志追踪上报 (业务背景) 6 | 7 | > 前端 JS 代码错误,浏览器都都会在控制台输出错误信息,以及出错的文件,行号,堆栈信息,这些错误很容易导致页面代码不执行,并且考虑到手机类型五花八门,浏览器内核以及版本的差异性,前端代码机型兼容性问题,并不能将所有的手机都拿来适配,前端错误日志上报是一个较好的解决方案 8 | 9 | ### 安装 Installation 10 | 11 | **直接下载** 12 | 点击下载 [error.js](https://github.com/ecitlm/js-log-report/blob/master/src/error.min.js)直接在你的页面中引用 13 | 14 | ``` 15 | 16 | ``` 17 | 18 | 或者引用 `jsDelivr CDN:` 19 | 20 | ``` 21 | 22 | ``` 23 | 24 | **npm 模块安装** 25 | 26 | ``` 27 | npm install js-log-report 28 | ``` 29 | 30 | ``` 31 | import errLogReport from 'js-log-report' 32 | // 方法调用查看 如何使用 33 | 34 | ``` 35 | 36 | ### 日志上报哪些数据 37 | 38 | 1.通过 `window.onerror` 可以获取 `msg, url, line, col, error`等错误信息,JS 的错误行号、url 错误地址, 2.通过 `window.navigator.userAgent` 获取 设备浏览器的信息集合 39 | 如: 40 | 41 | ``` 42 | User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1 43 | ``` 44 | 45 | 3. `os_version` 系统版本号 46 | 4. `browser` 浏览器类型 `Opera` `FF` `Chrome` `Safari` `IE` 47 | 48 | ```javascript 49 | var defaults = { 50 | ua: window.navigator.userAgent, 51 | browser: '', 52 | os: '', 53 | osVersion: '', 54 | errUrl: window.location.href, 55 | msg: '', // 错误的具体信息 56 | url: '', // 错误所在的url 57 | line: '', // 错误所在的行 58 | col: '', // 错误所在的列 59 | error: '' // 具体的error对象 60 | } 61 | ``` 62 | 63 | 具体上报字段可查看表结构 64 | 65 | ### 如何实现错误上报 66 | 67 | 1.实现错误日志收集 收集 onerror 错误参数,以及自定义的参数 2.核心`window.onerror`,重写该方法、在此中捕获异常错误信息、并且将错误信息发送至服务器接口 68 | 大致代码如下 69 | 70 | ```javascript 71 | window.onerror = function(msg, url, line, col, error) { 72 | ajax({ 73 | url: 'xxx/api/sendError', // 请求地址 74 | type: 'POST', // 请求方式 75 | data: data, // 请求参数 76 | dataType: 'json', 77 | success: function(response, xml) { 78 | // success 79 | }, 80 | fail: function(status) { 81 | // error 82 | } 83 | }) 84 | } 85 | ``` 86 | 87 | ### 如何使用 88 | 89 | > 使用如`index.html`所示,导入以下代码在页面 head 中,并且优先于其他 JS 文件加载 90 | 91 | ```html 92 | 93 | 103 | ``` 104 | 105 | ![上报数据样式](http://wx4.sinaimg.cn/mw690/0060lm7Tly1ftn2k22h1cj30ko062dg8.jpg) 106 | 107 | ### 数据上报表结构 108 | 109 | ```mysql 110 | DROP TABLE IF EXISTS `j_log`; 111 | CREATE TABLE `j_log` ( 112 | `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'id号', 113 | `os_version` char(10) DEFAULT NULL COMMENT '系统版本号', 114 | `msg` varchar(255) DEFAULT NULL COMMENT '错误信息', 115 | `error_url` varchar(255) DEFAULT NULL COMMENT '错误所在的url', 116 | `line` int(10) DEFAULT NULL COMMENT '错误所在的行', 117 | `col` int(10) DEFAULT NULL COMMENT '错误所在的列', 118 | `error` varchar(255) DEFAULT NULL COMMENT '具体的error对象', 119 | `url` varchar(255) DEFAULT NULL, 120 | `browser` varchar(255) DEFAULT NULL COMMENT '浏览器类型', 121 | `product_name` char(255) CHARACTER SET utf8 DEFAULT '' COMMENT '产品名称', 122 | `error_time` char(20) DEFAULT NULL COMMENT '时间戳', 123 | `os` char(10) DEFAULT NULL COMMENT '系统类型', 124 | `extend` varchar(255) DEFAULT NULL COMMENT '业务扩展字段、保存JSON字符串', 125 | `ua` varchar(255) DEFAULT NULL, 126 | PRIMARY KEY (`id`) 127 | ) ENGINE=MyISAM AUTO_INCREMENT=55 DEFAULT CHARSET=utf8; 128 | 129 | ``` 130 | 131 | ### 源代码 132 | 133 | [GitHub](https://github.com/ecitlm/js-log-report) 134 | 135 | ### 缺点 136 | 137 | 对于压缩的代码,报错信息没法定位到具体是什么地方报错了,这里没有去详细研究,阮一峰老师有篇相关文章 138 | [JavaScript Source Map 详解](http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html) 139 | 140 | #### License 141 | 142 | MIT licensed. 143 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples-errLogReport 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 |

请查看控制台可看到上报数据

27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-log-report", 3 | "version": "1.0.2", 4 | "description": "js-log-report", 5 | "main": "src/error.min.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ecitlm/js-log-report.git" 15 | }, 16 | "author": "ecitlm", 17 | "keywords": [ 18 | "jslog", 19 | "badjs", 20 | "js-log-report", 21 | "errorlog" 22 | ], 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/ecitlm/js-log-report/issues" 26 | }, 27 | "homepage": "https://github.com/ecitlm/js-log-report#readme" 28 | } 29 | -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ecitlm 3 | * @Date: 2018-06-21 09:39:59 4 | * @Last Modified by: ecitlm 5 | * @Last Modified time: 2018-06-21 13:36:53 6 | */ 7 | ;(function () { 8 | if (window.errLogReport) { 9 | return window.errLogReport 10 | } 11 | /* 12 | *ajax封装 13 | */ 14 | function ajax (options) { 15 | options = options || {} 16 | options.type = (options.type || 'GET').toUpperCase() 17 | options.dataType = options.dataType || 'json' 18 | var params = formatParams(options.data) 19 | 20 | if (window.XMLHttpRequest) { 21 | var xhr = new XMLHttpRequest() 22 | } else { 23 | var xhr = new ActiveXObject('Microsoft.XMLHTTP') 24 | } 25 | 26 | xhr.onreadystatechange = function () { 27 | if (xhr.readyState == 4) { 28 | var status = xhr.status 29 | if (status >= 200 && status < 300) { 30 | options.success && options.success(xhr.responseText, xhr.responseXML) 31 | } else { 32 | options.fail && options.fail(status) 33 | } 34 | } 35 | } 36 | 37 | if (options.type == 'GET') { 38 | xhr.open('GET', options.url + '?' + params, true) 39 | xhr.send(null) 40 | } else if (options.type == 'POST') { 41 | xhr.open('POST', options.url, true) 42 | // 设置表单提交时的内容类型 43 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') 44 | xhr.send(params) 45 | } 46 | } 47 | 48 | /* 49 | *格式化参数 50 | */ 51 | function formatParams (data) { 52 | var arr = [] 53 | for (var name in data) { 54 | arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name])) 55 | } 56 | arr.push(('v=' + Math.random()).replace('.', '')) 57 | return arr.join('&') 58 | } 59 | 60 | /* 61 | * 合并对象,将配置的参数也一并上报 62 | */ 63 | function cloneObj (oldObj) { // 复制对象方法 64 | if (typeof (oldObj) !== 'object') return oldObj 65 | if (oldObj == null) return oldObj 66 | var newObj = new Object() 67 | for (var prop in oldObj) { newObj[prop] = oldObj[prop] } 68 | return newObj 69 | }; 70 | 71 | function extendObj () { // 扩展对象 72 | var args = arguments 73 | if (args.length < 2) { return } 74 | var temp = cloneObj(args[0]) // 调用复制对象方法 75 | for (var n = 1, len = args.length; n < len; n++) { 76 | for (var index in args[n]) { 77 | temp[index] = args[n][index] 78 | } 79 | } 80 | return temp 81 | } 82 | 83 | function getSystemVersion () { 84 | var ua = window.navigator.userAgent 85 | if (ua.indexOf('CPU iPhone OS ') >= 0) { 86 | return ua.substring(ua.indexOf('CPU iPhone OS ') + 14, ua.indexOf(' like Mac OS X')) 87 | } else if (ua.indexOf('Android ') >= 0) { 88 | return ua.substr(ua.indexOf('Android ') + 8, 3) 89 | } else { 90 | return 'other' 91 | } 92 | } 93 | 94 | /** 95 | 获取浏览器类型 96 | */ 97 | function getBrowser () { 98 | var userAgent = navigator.userAgent // 取得浏览器的userAgent字符串 99 | var isOpera = userAgent.indexOf('Opera') > -1 100 | if (isOpera) { 101 | return 'Opera' 102 | }; // 判断是否Opera浏览器 103 | if (userAgent.indexOf('Firefox') > -1) { 104 | return 'FF' 105 | } // 判断是否Firefox浏览器 106 | if (userAgent.indexOf('Chrome') > -1) { 107 | return 'Chrome' 108 | } 109 | if (userAgent.indexOf('Safari') > -1) { 110 | return 'Safari' 111 | } // 判断是否Safari浏览器 112 | if (userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 && !isOpera) { 113 | return 'IE' 114 | }; // 判断是否IE浏览器 115 | } 116 | 117 | /** 118 | 获取设备是安卓、IOS 还是PC端 119 | */ 120 | function getDevices () { 121 | var u = navigator.userAgent, app = navigator.appVersion 122 | if (/AppleWebKit.*Mobile/i.test(navigator.userAgent) || (/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/.test(navigator.userAgent))) { 123 | if (window.location.href.indexOf('?mobile') < 0) { 124 | try { 125 | if (/iPhone|mac|iPod|iPad/i.test(navigator.userAgent)) { 126 | return 'iPhone' 127 | } else { 128 | return 'Android' 129 | } 130 | } catch (e) {} 131 | } 132 | } else if (u.indexOf('iPad') > -1) { 133 | return 'iPhone' 134 | } else { 135 | return 'Android' 136 | } 137 | } 138 | 139 | /* 140 | * 默认上报的错误信息 141 | */ 142 | var defaults = { 143 | ua: window.navigator.userAgent, 144 | browser: getBrowser(), 145 | os: getDevices(), 146 | osVersion: getSystemVersion(), 147 | errUrl: window.location.href, 148 | msg: '', // 错误的具体信息 149 | url: '', // 错误所在的url 150 | line: '', // 错误所在的行 151 | col: '', // 错误所在的列 152 | error: '' // 具体的error对象 153 | } 154 | 155 | /** 156 | * 核心代码区 157 | **/ 158 | var errLogReport = function (params) { 159 | if (!params.url) { return } 160 | window.onerror = function (msg, url, line, col, error) { 161 | // 采用异步的方式,避免阻塞 162 | setTimeout(function () { 163 | // 不一定所有浏览器都支持col参数,如果不支持就用window.event来兼容 164 | col = col || (window.event && window.event.errorCharacter) || 0 165 | 166 | defaults.url = url 167 | defaults.line = line 168 | defaults.col = col 169 | 170 | if (error && error.stack) { 171 | // 如果浏览器有堆栈信息,直接使用 172 | defaults.msg = error.stack.toString() 173 | } else if (arguments.callee) { 174 | // 尝试通过callee拿堆栈信息 175 | var ext = [] 176 | var fn = arguments.callee.caller 177 | var floor = 3 // 这里只拿三层堆栈信息 178 | while (fn && (--floor > 0)) { 179 | ext.push(fn.toString()) 180 | if (fn === fn.caller) { 181 | break// 如果有环 182 | } 183 | fn = fn.caller 184 | } 185 | defaults.msg = ext.join(',') 186 | } 187 | // 合并上报的数据,包括默认上报的数据和自定义上报的数据 188 | var reportData = extendObj(params.data || {}, defaults) 189 | console.log(reportData) 190 | 191 | // 把错误信息发送给后台 192 | ajax({ 193 | url: params.url, // 请求地址 194 | type: 'POST', // 请求方式 195 | data: reportData, // 请求参数 196 | dataType: 'json', 197 | success: function (response, xml) { 198 | // 此处放成功后执行的代码 199 | params.successCallBack && params.successCallBack(response, xml) 200 | }, 201 | fail: function (status) { 202 | // 此处放失败后执行的代码 203 | params.failCallBack && params.failCallBack(status) 204 | } 205 | }) 206 | }, 0) 207 | 208 | return true // 错误不会console浏览器上,如需要,可将这样注释 209 | } 210 | } 211 | 212 | window.errLogReport = errLogReport 213 | })() 214 | 215 | if (typeof(module) !== 'undefined'){ 216 | module.exports = window.errLogReport 217 | } 218 | else if (typeof define === 'function' && define.amd) { 219 | define([], function () { 220 | 'use strict' 221 | return window.errLogReport 222 | }) 223 | } -------------------------------------------------------------------------------- /src/error.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};!function(){if(window.errLogReport)return window.errLogReport;function u(n){(n=n||{}).type=(n.type||"GET").toUpperCase(),n.dataType=n.dataType||"json";var e=function(e){var n=[];for(var o in e)n.push(encodeURIComponent(o)+"="+encodeURIComponent(e[o]));return n.push(("v="+Math.random()).replace(".","")),n.join("&")}(n.data);if(window.XMLHttpRequest)var o=new XMLHttpRequest;else o=new ActiveXObject("Microsoft.XMLHTTP");o.onreadystatechange=function(){if(4==o.readyState){var e=o.status;200<=e&&e<300?n.success&&n.success(o.responseText,o.responseXML):n.fail&&n.fail(e)}},"GET"==n.type?(o.open("GET",n.url+"?"+e,!0),o.send(null)):"POST"==n.type&&(o.open("POST",n.url,!0),o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),o.send(e))}function c(){var e=arguments;if(!(e.length<2)){for(var n=function(e){if("object"!==(void 0===e?"undefined":_typeof(e)))return e;if(null==e)return e;var n=new Object;for(var o in e)n[o]=e[o];return n}(e[0]),o=1,r=e.length;o