推动产品演进—前端监控埋点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种前端埋点方式》

相关推荐
alikami几秒前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda32 分钟前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡34 分钟前
lodash常用函数
前端·javascript
emoji11111144 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
User_undefined1 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
麦兜*1 小时前
轮播图带详情插件、uniApp插件
前端·javascript·uni-app·vue
陈大爷(有低保)1 小时前
uniapp小案例---趣味打字坤
前端·javascript·vue.js
博客zhu虎康1 小时前
ElementUI 的 form 表单校验
前端·javascript·elementui
CoderLiu2 小时前
用Rust写了一个css插件,sass从此再见了
前端·javascript·前端框架