监控什么:
- 错误统计
- 行为日志埋点
- PV/UV统计
主要流程:
数据采集-日志上报-日志查询
数据采集:采集系统的监控数据,包括PV,UV和用户行为及前端报错的数据。 日志上报:上报将第一步采集的数据发送到服务端 日志查询:在后台查询我们采集上报的数据,包括对数据进行分析。 sdk主要对其前两部分实现
- 引入了path和HtmlWebpackPlugin两个模块,用于获取路径和生成 HTML 文件。
- exports 语句将配置对象导出,包括了入口文件、上下文目录、开发模式、输出目录、输出文件名、开发服务器、插件等多个属性。
- entry 指定了入口文件为 './src/index.js'。
- context 指定了上下文目录为当前工作目录,即 process.cwd()。
- mode 指定了开发模式为 development。
- output 配置了输出目录为 './dist/' 目录,输出文件名为 'monitor.js'。
- devServer 配置了开发服务器,指定了服务器的内容目录为 './dist/' 目录。
- plugins 数组里配置了 HtmlWebpackPlugin 插件,它会根据模板文件 './src/index.html' 自动生成 HTML 文件,并将生成的文件嵌入到 head 标签中。
错误监控
思路
引入监控文件,丢出报错,触发addEventListener事件,开启一系列报错定位
window.onerror错误捕获事件
当JavaScript运行时错误或者语法错误发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。
参数
- message:错误信息(字符串)。
- source:发生错误的脚本URL(字符串)
- lineno:发生错误的行号(数字)
- colno:发生错误的列号(数字)
- error:Error对象(对象)
在不同域中的语法发生错误的时候,为避免信息泄露,语法错误的细节将不会报告,而代之简单的"Script error."。
addEventListener参数
可以监听的参数事件参考 | MDN (mozilla.org)
el.addEventListener(type, listener[, useCapture])
-
el:事件对象。比如,某个标签,window,document 对象等等。 -
type:事件类型,click、mouseenter 等 -
listener:事件处理函数,事件发生时,就会触发该函数运行。 -
useCapture:布尔值,规定是否是捕获型,默认为 false(冒泡)。 为true时捕获,false时冒泡。因为是可选的,往往也会省略它。
event
第三个参数
el.addEventListener(type, listener, {
capture: false, // === useCapture
once: false, // 是否设置单次监听,if true,会在调用后自动销毁listener
passive: false // 是否让 阻止默认行为(preventDefault()) 失效,if true, 意味着listener永远不远调用preventDefault方法
})
// 新增参数的三个属性,默认值都是 false。
getLastEvent (捕获最后一个操作的方法)
let lastEvent;
['click', 'touchstart', 'mousedown', 'keydown'].forEach(eventType => {
document.addEventListener(eventType, (event) => {
lastEvent = event;
},
{
capture: true,//捕获阶段
passive: true //默认不阻止默认事件
})
})
获取stack
function getLines(stack) {
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join('^')
}
实现getSelector
body-div#container-div.content-input
- 通过getLastEvent获得一系列数据,其中就有path数组
- 顺序从小到大所以要反转
- 筛选出去document和window
function getSelectors(path) {
path.reverse().filter(element => {
return element !== document && element !== window
}).map(element => {
let selector = ''
if (element.id) {
return `${element.nodeName.toLowerCase()}#${element.id}`
}
else if (elelment.className && typeof element.className === 'string') {
return `${element.nodeName.toLowerCase()}#${element.className}`
}
else {
selector = element.nodeName.toLowerCase()
}
}).join('')
}
export default function (path) {
if (Array.isArray(path)) {
return getSelectors(path)
}
else {//也有可有是一个对象 -- 比如资源加载错误
let path = [];
while (pathsOrTarget) {
path.push(pathsOrTarget);
pathsOrTarget = pathsOrTarget.parentNode;
}
return getSelectors(path);
}
}
初步log打印出来结果
上报
tracker 追踪器上报
在jsError中 加入tracker.send(log)
Promise错误
通过监听unhandledrejection事件,可以捕获未处理的Promise错误。
window.addEventListener('unhandledrejection', (event) => {
console.log(event);
let lastEvent = getLastEvent();//最后一个交互事件
let message;
let filename;
let line = 0;
let column = 0;
let stack = '';
let reason = event.reason;
if (typeof reason === 'string') {
message = reason;
} else if (typeof reason === 'object') {//说明是一个错误对象
message = reason.message;
//at http://localhost:8080/:23:38
if (reason.stack) {
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
filename = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = getLines(reason.stack);
}
tracker.send({
kind: 'stability',//监控指标的大类
type: 'error',//小类型 这是一个错误
errorType: 'promiseError',//JS执行错误
message,//报错信息
filename,//哪个文件报错了
position: `${line}:${column}`,
stack,
//body div#container div.content input
selector: lastEvent ? getSelector(lastEvent.path) : '' //代表最后一个操作的元素
});
}, true);
资源加载错误(可以被addEventListener'error'捕获,所以需要判别一下)
window.addEventListener('error', function (event) {//错误事件对象
let lastEvent = getLastEvent();//最后一个交互事件
//这是一个脚本加载错误
if (event.target && (event.target.src || event.target.href)) {
tracker.send({
kind: 'stability',//监控指标的大类
type: 'error',//小类型 这是一个错误
errorType: 'resourceError',//js或css资源加载错误
filename: event.target.src || event.target.href,//哪个文件报错了
tagName: event.target.tagName,//SCRIPT
//body div#container div.content input
selector: getSelector(event.target) //代表最后一个操作的元素
});
} else {
tracker.send({
kind: 'stability',//监控指标的大类
type: 'error',//小类型 这是一个错误
errorType: 'jsError',//JS执行错误
message: event.message,//报错信息
filename: event.filename,//哪个文件报错了
position: `${event.lineno}:${event.colno}`,
stack: getLines(event.error.stack),
//body div#container div.content input
selector: lastEvent ? getSelector(lastEvent.path) : '' //代表最后一个操作的元素
});
}
}, true);
send详情
let host = 'cn-beijing.log.aliyuncs.com';//主机名-固定
let project = 'xinsu';//项目名
let logStore = 'xinsu-store';//存储名
send(data = {}) {
this.xhr.open('POST', this.url, true);
let body = JSON.stringify(data)
this.xhr.setRequestHeader('Content-Type', 'application/json');//请求体类型
this.xhr.setRequestHeader('x-log-apiversion', '0.6.0');//版本号
this.xhr.setRequestHeader('x-log-bodyrawsize', body.length);//请求体的大小
this.xhr.onload = function () {
console.log(this.xhr.response)
}
this.xhr.onerror = function (error) {
console.log(error)
}
this.xhr.send(body)
}
getExtraData 额外数据
let userAgent = require('userAgent')//User Agent中文名为用户代理,查看浏览器信息
function getExtraData() {
return {
title: document.title,
url: location.href,
timestamp: Date.now(),
userAgent: userAgent.parse(navigator.userAgent).name,
//用户ID
}
}
XMLHttpRequest
继承的属性
-
responseText:包含响应主体返回的文本; -
responseXML:如果响应的内容类型为text/xml或application/xml时,该属性将保存包含着响应数据的 XML DOM 文档; -
status:响应的 HTTP 状态码; -
statusText:HTTP 状态的原因短语; -
readyState:表示在「请求/响应」过程中当前的活动阶段;
继承的方法
-
.open():用于准备启动一个 AJAX 请求; -
.setRequestHeader():用于设置请求头部信息; -
.send():用于发送 AJAX 请求; -
.getResponseHeader():用于获得响应头部信息; -
.getAllResponseHeader():用于获得一个包含所有头部信息的长字符串; -
.abort():用于取消异步请求;
Navigator.userAgent
Navigator对象是包含有关 浏览器 的信息。 userAgent | 返回由客户机发送服务器的 user-agent 头部的值。 | userAgent 属性是一个只读的字符串,声明了浏览器用于 HTTP 请求的用户代理头的值。
tracker 部分代码
let host = 'cn-beijing.log.aliyuncs.com';//主机名-固定
let project = 'xinsu';//项目名
let logStore = 'xinsu-store';//存储名
let userAgent=userAgent.parse(navigator.userAgent).name
//用户ID
function getExtraData() {
return {
title: document.title,
url: location.href,
timestamp: Date.now(),
userAgent: userAgent.parse(navigator.userAgent).name,
//用户ID
}
}
class SendTracker {
constructor() {
//调用putWebTracking接口将多条日志合并一次请求,进行采集
this.url = '${project}.${host}/logstores/${logStore}/track';
this.xhr = new XMLHttpRequest;
}
send(data = {}) {
this.xhr.open('POST', this.url, true);
//格式
let body = JSON.stringify({
_logs_: [log]
})
let log = { ...getExtraData, ...data }
//对象的值不能是数字-阿里云规定
for (let key in log) {
if (typeof log[key] === 'number') {
log[key] = `${log[key]}`
}
}
this.xhr.setRequestHeader('Content-Type', 'application/json');//请求体类型
this.xhr.setRequestHeader('x-log-apiversion', '0.6.0');//版本号
this.xhr.setRequestHeader('x-log-bodyrawsize', body.length);//请求体的大小
this.xhr.onload = function () {
console.log(this.xhr.response)
}
this.xhr.onerror = function (error) {
console.log(error)
}
this.xhr.send(body)
}
}
export default new SendTracker()
开通日志服务 (SLS)使用阿里云日志服务
[阿里文档](PutWebtracking (aliyun.com))
请求示例
业内主流自建前端监控一般都是用image上报,而不是http上报。 gif图片做上传,图片速度快,没有跨域问题
POST /logstores/ali-test-logstore/track HTTP/1.1 Host:ali-test-project.cn-hangzhou.log.aliyuncs.com Content-Type:application/json { "__topic__" : "topic", "__source__" : "source", "__logs__" : [ { "key1": "value1", "key2": "value2" }, { "key1": "value1", "key2": "value2" } ], "__tags__" : { "tag1": "value1", "tag2": "value2" } }
打印上报的信息
接口异常采集脚本
成功和失败的返回
实例
devServer: {
before(router) {
router.get('/success',function (req,res) {
res.json({id:1})//200
});
router.post('/error',function (req,res) {
res.sendStatus(500)//200
});
}
}
这段代码是在webpack配置中的devServer选项下,使用了before函数来定义路由规则。具体来说:
- 在访问
/success路径时,会返回{id:1}的json数据以及200状态码。- 在访问
/error路径时,会返回500状态码以及200状态码。这段代码适用于在开发环境下模拟API接口的情况,通过配置devServer可以简单地实现路由和数据的模拟,方便开发调试。
xhr.js
import tracker from '...'
export default function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest
let oldOpen = XMLHttpRequest.prototype.open;//存储老的方法
XMLHttpRequest.prototype.open = function (method, url, async) {
this.logData = { method, url, async }
return oldOpen.apply(this, arguments)
}
let oldSend = XMLHttpRequest.prototype.send;//存储老的方法
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
let startTime = Date.now() //在发送之前
let handler = (type) => (event) => {
let duration = Date.now() - startTime //实际时间
let status = this.status;//200 500
let statusText = this.statusText;// OK Server Error
tracker.send({
})
}
this.addEventListener('load', handler('load'), false);
this.addEventListener('error', handler('error'), false);
this.addEventListener('abort', handler('abort'), false);
}
return oldSend.apply(this, arguments)
}
}