前端监控学习

217 阅读7分钟

监控什么:

  • 错误统计
  • 行为日志埋点
  • PV/UV统计

主要流程:

数据采集-日志上报-日志查询

数据采集:采集系统的监控数据,包括PV,UV和用户行为及前端报错的数据。 日志上报:上报将第一步采集的数据发送到服务端 日志查询:在后台查询我们采集上报的数据,包括对数据进行分析。 sdk主要对其前两部分实现

  1. 引入了path和HtmlWebpackPlugin两个模块,用于获取路径和生成 HTML 文件。
  2. exports 语句将配置对象导出,包括了入口文件、上下文目录、开发模式、输出目录、输出文件名、开发服务器、插件等多个属性。
  3. entry 指定了入口文件为 './src/index.js'。
  4. context 指定了上下文目录为当前工作目录,即 process.cwd()。
  5. mode 指定了开发模式为 development。
  6. output 配置了输出目录为 './dist/' 目录,输出文件名为 'monitor.js'。
  7. devServer 配置了开发服务器,指定了服务器的内容目录为 './dist/' 目录。
  8. plugins 数组里配置了 HtmlWebpackPlugin 插件,它会根据模板文件 './src/index.html' 自动生成 HTML 文件,并将生成的文件嵌入到 head 标签中。

错误监控

思路

引入监控文件,丢出报错,触发addEventListener事件,开启一系列报错定位

window.onerror错误捕获事件

当JavaScript运行时错误或者语法错误发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。

参数

  1. message:错误信息(字符串)。
  2. source:发生错误的脚本URL(字符串)
  3. lineno:发生错误的行号(数字)
  4. colno:发生错误的列号(数字)
  5. 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

image.png

第三个参数


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数组

image.png

  • 顺序从小到大所以要反转
  • 筛选出去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打印出来结果

image.png image.png

上报

tracker 追踪器上报

在jsError中 加入tracker.send(log)

Promise错误

通过监听unhandledrejection事件,可以捕获未处理的Promise错误。 image.png

image.png

   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'捕获,所以需要判别一下)

image.png

    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/xmlapplication/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" } }

image.png

打印上报的信息

image.png

接口异常采集脚本

成功和失败的返回

image.png

实例

企业微信截图_1683854512929.png

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)
    }
}