推动产品演进—前端监控埋点SDK开发

前言

为什么要做前端监控?

我们在写程序时也许会考虑以下几个问题:

  • 我们写的功能是否有人在用?

  • 新的功能是否稳定?有没有逃逸的线上缺陷?

  • 产品的数据是否可以积累?产品是不是还有优化的空间?

在当今的互联网世界中,网站和应用程序的 性能稳定性用户体验 至关重要。我们需要一个脚本能实时监控我们的程序,对用户使用情况进行跟踪,对程序运行错误进行提前预警和定位,对产品使用数据进行累计从而为程序产生 可持续 的发展方向。

今天就由小凌带大家搭建一个前端埋点监控小 demo,让大家对前端埋点监控有个初步的认知。

前端监控目标

我们需要一个这样的程序,它可以收集用户日常产生的错误从而提高产品的稳定性。

能监控用户的体验

最好在使用上能更简便,无需复杂的学习和繁琐的引入方式。

对老业务无影响,后期好维护,能可持续发展。

范围与实现流程

前端目前的需要监控的埋点位置有 页面加载按钮点击表单提交JavaScript 报错页面加载超时用户请求完成等。

今天我们主要对 JavaScript 报错进行埋点。监测若 JavaScript 代码运行出错则向日志服务器发送一条埋点请求,带上相关报错信息。

该流程主要分为如下几个阶段:

  1. 用户触发页面报错

  2. 监听到出错并调用相关方法

  3. 收集用户信息以及报错位置信息

  4. 请求发送埋点接口,将数据发送至云平台或后台

  5. 查看相关埋点信息在云平台或后台相关的记录是否正确

实践

构建项目

使用webpack初始化一个项目

csharp 复制代码
npm init -y

下载我们本次需要的依赖包

css 复制代码
npm install html-webpack-plugin user-agent webpack webpack-cli webpack-dev-server

完整 webpack.config.js 配置

js 复制代码
const  path = require('path');

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    mode: 'development',
    context: process.cwd(),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devServer:{
        static: path.resolve(__dirname, 'dist'),
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./src/index.html',
            inject:'head',
        })
    ]
}

目录布局与数据构思

我们需要提前规划好该项目的整体文件布局

也需要提前构思好需要记录的数据有哪些?这些属性名分别是什么?

主页入口

我们需要编写一个可以触发 JavaScript 代码报错的场景,这边我们使用 a.b.c 找不到属性的错误出发场景。

src/index.html

html 复制代码
<!--html-->
<button class="error-button" onclick="errorClick()">点此抛出错误</button>
<!--js-->
<script>
        function errorClick(){
            // 点击时候抛出错误
            window.a.b = 'no find error'
        }
</script>

对于需要引入的主要依赖 JavaScript 文件,我们直接让其引入 lib 文件夹中的方法并执行。

src/index.js

js 复制代码
import { injectJsError } from "./monitor/lib/jsError.js";
injectJsError();

错误捕获

在 lib 中我们申明一个专门捕获 JavaScript 报错的文件。

src\monitor\lib\jsError.js

js 复制代码
import { getLastEvent, getSelector, getLines } from '../utils';
import tracker from '../utils/tracker';
// 对js报错进行监听
export const injectJsError = () => {
    window.addEventListener('error', (e) => {
        const lastEvent = getLastEvent(); // 获取最后一个事件
        let log = {
            kind: 'stability',
            type: 'error', // 埋点类型
            errorType: 'JSError', // 具体错误
            url: window.location.href, // 访问路径
            message: e.message, // 错误信息
            fileName: e.fileName, // 错误文件名
            position: `${e.lineno}:${e.colno}`, // 错误位置
            stack: getLines(e.error.stack), // 错误堆栈
            selector: lastEvent ? getSelector(lastEvent.composedPath()) : '' // 最后一个操作的元素
        }
        // 发送埋点数据
        tracker.send(log)
    })
}

可以看到,我们在这个 js 中监听了 window 的 error 事件,并在其参数中获取到了我们想要的信息。

这边前面我们拟定的传输数据属性固定就起到了作用,我们很明确我们需要什么内容。

为保证该 js 的低耦合,我们把相关数据转换和接口请求放到了外部引入。

数据转换与获取

堆栈内容可读性转换

src\monitor\utils\index.js

js 复制代码
export const getLines = (stack) => {
    return stack.split('\n').reverse().map(item => {
        return item.replace(/^\s+at\s+/g, '')
    }).join('\n')
}

最后触发元素记录

这里我们在 js 载入时先对所有的用户操作进行记录,如果触发了相关事件则替换最后触发元素的变量。

src\monitor\utils\index.js

js 复制代码
const EVENTS = ['click','touchstart','mousedown','keyDown','mouseover'];
let lastEvent;
EVENTS.forEach((eventType) => {
    document.addEventListener(eventType, (event) => {
        lastEvent = event
    },
    {
        capture: true, // 捕获阶段
        passive: true // 冒泡阶段
    })
})

export const getLastEvent = () => {
    return lastEvent
}

通过 getLastEvent 方法我们便可获得用户最后操作的元素。

获取最终触发元素的路径

这个仍然是一个转换方法,我们需要先取出无用的最外层 documentwindow

src\monitor\utils\index.js

js 复制代码
export const getSelector = (path) => {
    let selectorPathStr = ''
    if(path.length){
        selectorPathStr = path.reverse()
        .filter(element =>  element !== document && element !== window )
        .map(element => {
            let selector = ''
            if(element.id){
                return `${element.nodeName.toLowerCase()}#${element.id}`
            }else if(element.className && typeof element.className === 'string'){
                return `${element.nodeName.toLowerCase()}.${element.className}`
            }else{
                selector = element.nodeName.toLowerCase()
            }
            return selector
        }).join(' ')
    }
    return selectorPathStr
}

这样我们便可以得到最终触发问题的节点位置,如下:

阿里云日志SLS对接

我们需要一个 接口 来获取我们的埋点信息并进行持久化。此处我们使用目前比较成熟 阿里云日志SLS ,将我们的日志信息上传到云。当然,你也可以搭建后台地址,提供一个可持久化数据的接口来保存埋点信息。

Project 创建

创建 Project

此处选择打开

选择接入方式 WebTracking

下一步

打开控制台发现无数据

接口对接

找到 PutWebtracking 帮助文档,查看对接具体参数。

在真正接通之前,我们可以用 Postman 来对接口进行测试。

js 复制代码
http://${PROJECT}.log.aliyuncs.com/logstores/${STORE}/track

其中 PROJECTSTORE 都可以在控制台上获得。

额外参数

当然我们还要获取用户的操作时间和浏览器与系统信息,方便我们定位错误位置。

src\monitor\utils\tracker.js

js 复制代码
// 获取额外参数
const getExtraData = () => {
    return {
        title: document.title, // 网页标题
        timestamp: `${new Date().getTime()}`, // 时间戳(阿里云只接收String格式字符串)
        userAgent: userAgent.parse(navigator.userAgent).full // 用户系统与浏览器信息
    }
}

// 发送给接口的日志信息
const log = {
            ...getExtraData(),
            ...data // 前面封装的信息
        }

完整的请求方法

js 复制代码
class SendTracker{
    constructor(){
        this.url = `${HOST}/logstores/${STORE}/track`
        this.xhr = new XMLHttpRequest()
    }
    send(data = {}){
        this.xhr.open('POST', this.url, true)
        const log = {
            ...getExtraData(),
            ...data
        }
        for (let key in log){
            // 阿里云限制必须要String
            if(typeof log[key] === 'unmber'){
                log[key] = `${log[key]}`
            }
        }
        const body = JSON.stringify({
            "__topic__": STORE,
            "__source__": "my-source",
            "__logs__": [
                {...log}
            ],
        })
        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.send(body)
        this.xhr.onload = () => {
            console.log(this.xhr.response)
        }

        this.xhr.onerror = (error) => {
            console.log(error)
        }
    }
}
export default new SendTracker;

测试

一切都设置成功后,当我们点击"点此抛出错误"按钮的时候,就会向阿里云发送一次请求

在后台我们便可看到响应的数据已经写入

打包后点击 index.html 中的按钮正常请求

如此,外部使用只要引入打包好的 bundle.js 便可引入我们的 埋点功能

优化

当然你也可以优化它:

1.使用 TypeScript 规范化它的变量类型和全局枚举设置。

  1. 使用 node.js 将数据存储为本地信息。只需要我们提供一个接口将数据接入本地即可。值得一提的是数据格式尽量保持一致,方便后续的数据迁移

仓库地址

前端监控SDKDemo

参考文档

《从零到一五小时彻底掌握前端监控埋点》

《分享3种前端埋点方式》

相关推荐
不瘦80斤不改名35 分钟前
JavaScript 基础语法完全指南
开发语言·javascript·ecmascript
peepeeman41 分钟前
vue组件透传
前端·javascript·vue.js
a1117762 小时前
细胞结构实验室(react 开源)
前端·javascript·开源·html
Dxy12393102162 小时前
JS如何获取元素高度
开发语言·javascript·ecmascript
豹哥学前端2 小时前
5分钟搞懂事件委托
前端·javascript·面试
绝世唐门三哥2 小时前
ES6 --- import/export 全解析
开发语言·前端·javascript
yqcoder2 小时前
JavaScript 异步基石:Promise 完全指南
开发语言·前端·javascript
代码煮茶2 小时前
Vue3 上传组件实战 | 从 0 封装大文件分片上传组件(断点续传 / 秒传 / 进度条)
javascript·vue.js
之歆3 小时前
DAY_25 JavaScript 原型、原型链与值类型/引用类型 ── 深度全解(下)
开发语言·javascript·ecmascript
咪饭只吃一小碗3 小时前
从变量提升到 V8 预编译,彻底搞懂 JS 执行机制
javascript