【前端团队】前端日志上报实践

4,740 阅读8分钟

相信大家在开发前端项目的时候都遇到过日志上报相关的需求吧,小到某个按钮的点击,大到某个流程的操作回溯,这些都属于日志上报的范畴。下面笔者将会较为详细的阐述一下日志、埋点以及日志上报之间的关系与开发实践。

① 前端为什么需要日志上报

监测网页应用运行状况

前端是产品与用户交流的桥梁,对前端有真正感知的是我们面向的所有用户。倘若用户在使用网页应用的过程中出现了异常,且没有反馈给应用开发人员,那么我们将永远无法感知到这个异常,它可能会无形中影响到成千上万个用户的体验,甚至可能直接影响产品的核心流程。

此时,我们就需要为前端项目装载日志上报的功能,实时的监测到应用中产生的报错,在适当的时机将这些错误日志上报到日志中心,帮助开发人员及时、准确感知、定位异常,并在影响范围还为扩大的时候完成修复。

了解用户操作行为

前端的日志上报除了可以为应用状况保驾护航,还可以帮助我们分析应用设计的合理性以及应用后续的迭代规划。这种时候,我们就需要对用户的操作行为进行针对性的日志上报,可能是页面中某几个按钮的点击事件,也可能是某个列表的滚动事件,甚至可能是某些页面的停留时长......

有了这些用户的操作日志,产品同事就可以从中进行归因分析,产出产品的优化、迭代规划,让应用更懂用户。

② 日志与埋点是什么关系

前面大致讲了一下前端日志的作用,接下来会对日志本身进行更深入的探索。除了日志,大家可能还会经常听到一个伴生词 -- 埋点

那么日志和埋点是什么关系呢?

简单来说,埋点就是一支支彩色笔,而日志则是一张纸,埋点每次被触发,就会按某些既定规则选出适合的颜色笔,往纸上记录些什么,而不会关心这张纸最终会被用来做什么。

为什么上面要说埋点是彩色笔呢,因为在实际的埋点操作中,是需要区分埋点类型的。我们常见的埋点类型有这几种:

用户行为埋点、异常监控埋点、曝光埋点......

不同类型的埋点,上报的数据都不太一样,行为埋点大多是用户与界面的交互;异常监控则是字面意思,它可能是JS语法的错误亦可能是一次失败的请求;曝光埋点则是更加精细化的用户行为记录,比如长列表中的某一项是否在用户的界面中出现过......

③ 日志上报的运作原理

以用户行为上报为例,假设界面上有一个按钮进行了埋点,那么用户在点击该按钮时将会产生一条日志,最终这条日志会经过一系列处理,沉淀到数仓中。

未命名文件.jpg

如上图,就是最简略的日志上报运作流程,接下来笔者将会展开讲讲底下这条链路的具体实践。

④ 日志上报的具体实践

承接上面运作原理的图,我们先来看第一步,统一埋点方法。它的本质就是触发一个通用的函数,这里称之为EventTrack

function handleBtnClick() {
    // 上报时机视业务要求而定,有可能在一开始就执行,也可能在函数执行的中途进行上报
    window.EventTrack({
        eventType: 'click',
        elemId: 'SEARCH',
        extraParams: {
            position: 'banner'
        }
    })

    // 一些业务逻辑...
}

可以看到,用户点击按钮触发handleBtnClick函数的时候,内部不仅执行了本该有的业务逻辑,还加上了一段EventTrack函数,里面传递了eventType以及elemId

eventType - 埋点事件类型,它可能是点击事件、页面初始化、页面激活、请求事件...

elemId - 埋点标识,后续进行埋点数据提取的重要标识

extraParams - 额外的上报内容,丰富埋点的含义帮助后续做更加精细、完善的分析

成功触发EventTrack之后,是不是就会立刻将传入的内容发送到日志服务呢?

这里并没有所谓最正确的做法,因为最终发送日志上报请求的方式有很多种,有些是GET请求、有些是请求一张图片而有些则是发送POST请求。不同的请求方式,发送日志内容的时机也不太一样。

如果是GET请求的方式进行日志上报,则一般是埋点触发后,产生日志条目就直接发送到日志服务。

如果是POST请求的方式,则有大多数会在项目中维护一个日志上报队列,从日志条数、上报间隔等维度进行日志条目的统一上报。

无论是使用哪种方式进行日志上报,都需要在上报前对对埋点的内容进行预处理,让它符合日志服务的要求。

一般来说,一条相对完整的日志条目需要有这些维度的内容:

const events = {
    /* 应用信息 */
    appInfo: {
        appId: '', // 应用ID,微信appid等
        appName: '', // 应用名称
        version: '', // 应用版本号,一般为业务版本号
        // ...
    },
    /**
     * 设备信息
     * ① 用于统计设备型号分布
     * ② 用于定位设备兼容问题
     */
    systemInfo: {
        ua: navigator.userAgent.toLowerCase(),
        deviceID: '', // 设备号
        // ...
    },
    /**
     * 用户信息
     * 多为用户特征标识,一般较为敏感
     * 多用于后续用户行为分析
     */
    userInfo: {
        openId: '',
        userId: '',
        //...
    },
    /**
     * 日志集合
     * 多条日志的集合,最终会和上面的信息一并发送到日志服务进行进一步解析沉淀
     */
    items: [
        {
            eventType: 'click',
            elemId: 'SEARCH',
            extraParams: {
                position: 'banner'
            },
            path: 'https://xxx.com/home?a=1',
        }
    ]
}

最后用一张图来总结一下整个日志上报的实践过程。

未命名文件 (1).jpg

⑤ 日志插件的可拓展性

紧接着上面的流程图,我们可以提炼出几个可供拓展的配置项。

export interface IReportOptions {
    /* 允许上报的事件类型 */
    acceptEventType: string[]

    /* 上报函数触发间隔 */
    sendTimeout: number

    /* 上报队列最大数量 */
    sendQueueSize: number

    /* 使用方传入的上报方法,参数为待上报队列 */
    sendFn: (content: IReportContent) => void

    /* 获取当前页面路由 */
    getCurrentPage: () => string

    /* 获取默认上报内容 */
    getInitialEventContent: () => IInitiaReportContent
}

export interface IEvent {
    eventType: string
    path: string
    elemId?: string
    createTime?: string
    extraParams?: object
}

export interface IInitiaReportContent {
    appInfo?: IAppInfo
    systemInfo?: ISystemInfo
    userInfo?: IUserInfo
}

export interface IReportContent extends IInitiaReportContent {
    items?: IEvent[]
    reportTime?: string
    [key: string]: any
}

也许大家会疑惑,为什么不将这些内容内置到插件里面去呢,还非得让使用方关注那么多东西。

这里涉及到易用性和灵活性的取舍,显而易见,笔者是更倾向于灵活性的,主要原因有以下两点:

跨端需求,后续团队必然会出现H5/小程序/WEB多端齐下的情况,能够用一套日志插件将会极大地减少额外开发资源的投入。

项目繁多,前端团队目前承载的项目数量较多并且各有特点,每个项目的产品团队对数据埋点的重心都不尽相同,自然在开发插件的时候不应该有过多内置的配置。

⑥ 日志插件的过渡方案(特例)

因笔者负责的项目暂时没有数据仓库相关的建设,也就没有沉淀日志的地方,所以项目初期打算先使用第三方埋点平台进行临时分析,这里使用的是友盟。

也许大家会疑惑,友盟有自己的SDK了为什么还需要在套一个自己的插件呢?

是的,乍一看确实没有必要,但是后续团队总会搭建起自己的数据中台,那时候就会弃用友盟转用自己的日志收集服务。

届时,埋点方式的替换必定会让前端同学们头疼,友盟提供的上报方式以及内容肯定不会与团队内数据中台的需求一致,自研的数据中台需要的数据必定是更加详尽、敏感的。

所以笔者预研了过渡方案,也就是下面将会讲到的单日志上报模式

export interface IReportOptions {
    // ......
    
    /**
     * 是否启动单日志上报模式
     * 如果启动单日志上报模式,则不会启动上报队列,其余特性不变
     */
    singleModel: boolean
}

启动了该模式,将会拦截进入日志队列的日志条目,并直接触发友盟提供的上报API。

这么一来,后续如果内部的日志服务研发完毕,我们可以在拦截日志条目的时候触发友盟的API并且向团队自身的日志服务发送日志。

再往后,数仓也准备好之后就可以直接将单日志模式关闭,并且统一删除友盟API的触发,完成从友盟到自研日志服务的无缝过渡。

⑦ 日志插件下载&用法

import EventTrack from 'EventTrack'

const eventTrack = new EventTrack({
    acceptEventType: ['onLaunch', 'onLoad', 'onShow', 'request', 'onError', 'click'],
    sendTimeout: 1000 * 5,
    sendQueueSize: 30,
    sendFn: e => {
        // 日志上报请求
        doReportSend(e)
    },
    getCurrentPage: () => window.location.href,
    getInitialEventContent: () => {
        return {
            appInfo: {
                appId: 'xxx',
                appName: 'appxxx',
                version: '1.0.0',
            },
            systemInfo: {
                ua: navigator.userAgent.toLowerCase(),
                deviceID: '',
            },
            userInfo: {
                // userId,
                // openId,
            },
        }
    },
})

export default eventTrack

通过上面的代码对EventTrack进行初始化,随后就可以用eventTrack来进行埋点上报了,不需要业务侧关心具体的上报过程。

最后附上项目地址,对大家有帮助的话欢迎star~

github.com/mykurisu/ev…

暂时不打算发布NPM包,还有不少功能可以集成进去,大家可以fork之后根据实际情况进行二开