前端监控 SDK 的一些技术要点原理分析(下)

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
简介: 前端监控 SDK 的一些技术要点原理分析(下)

Vue 路由变更渲染时间

首屏渲染时间我们已经知道如何计算了,但是如何计算 SPA 应用的页面路由切换导致的页面渲染时间呢?本文用 Vue 作为示例,讲一下我的思路。

export default function onVueRouter(Vue, router) {
    let isFirst = true
    let startTime
    router.beforeEach((to, from, next) => {
        // 首次进入页面已经有其他统计的渲染时间可用
        if (isFirst) {
            isFirst = false
            return next()
        }
        // 给 router 新增一个字段,表示是否要计算渲染时间
        // 只有路由跳转才需要计算
        router.needCalculateRenderTime = true
        startTime = performance.now()
        next()
    })
    let timer
    Vue.mixin({
        mounted() {
            if (!router.needCalculateRenderTime) return
            this.$nextTick(() => {
                // 仅在整个视图都被渲染之后才会运行的代码
                const now = performance.now()
                clearTimeout(timer)
                timer = setTimeout(() => {
                    router.needCalculateRenderTime = false
                    lazyReportCache({
                        type: 'performance',
                        subType: 'vue-router-change-paint',
                        duration: now - startTime,
                        startTime: now,
                        pageURL: getPageURL(),
                    })
                }, 1000)
            })
        },
    })
}

代码逻辑如下:

  1. 监听路由钩子,在路由切换时会触发 router.beforeEach() 钩子,在该钩子的回调函数里将当前时间记为渲染开始时间。
  2. 利用 Vue.mixin() 对所有组件的 mounted() 注入一个函数。每个函数都执行一个防抖函数。
  3. 当最后一个组件的 mounted() 触发时,就代表该路由下的所有组件已经挂载完毕。可以在 this.$nextTick() 回调函数中获取渲染时间。

同时,还要考虑到一个情况。不切换路由时,也会有变更组件的情况,这时不应该在这些组件的 mounted() 里进行渲染时间计算。所以需要添加一个 needCalculateRenderTime 字段,当切换路由时将它设为 true,代表可以计算渲染时间了。

错误数据采集

资源加载错误

使用 addEventListener() 监听 error 事件,可以捕获到资源加载失败错误。

// 捕获资源加载失败错误 js css img...
window.addEventListener('error', e => {
    const target = e.target
    if (!target) return
    if (target.src || target.href) {
        const url = target.src || target.href
        lazyReportCache({
            url,
            type: 'error',
            subType: 'resource',
            startTime: e.timeStamp,
            html: target.outerHTML,
            resourceType: target.tagName,
            paths: e.path.map(item => item.tagName).filter(Boolean),
            pageURL: getPageURL(),
        })
    }
}, true)

js 错误

使用 window.onerror 可以监听 js 错误。

// 监听 js 错误
window.onerror = (msg, url, line, column, error) => {
    lazyReportCache({
        msg,
        line,
        column,
        error: error.stack,
        subType: 'js',
        pageURL: url,
        type: 'error',
        startTime: performance.now(),
    })
}

promise 错误

使用 addEventListener() 监听 unhandledrejection 事件,可以捕获到未处理的 promise 错误。

// 监听 promise 错误 缺点是获取不到列数据
window.addEventListener('unhandledrejection', e => {
    lazyReportCache({
        reason: e.reason?.stack,
        subType: 'promise',
        type: 'error',
        startTime: e.timeStamp,
        pageURL: getPageURL(),
    })
})

sourcemap

一般生产环境的代码都是经过压缩的,并且生产环境不会把 sourcemap 文件上传。所以生产环境上的代码报错信息是很难读的。因此,我们可以利用 source-map 来对这些压缩过的代码报错信息进行还原。

当代码报错时,我们可以获取到对应的文件名、行数、列数:

{
    line: 1,
    column: 17,
    file: 'https:/www.xxx.com/bundlejs',
}

然后调用下面的代码进行还原:

async function parse(error) {
    const mapObj = JSON.parse(getMapFileContent(error.url))
    const consumer = await new sourceMap.SourceMapConsumer(mapObj)
    // 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
    const sources = mapObj.sources.map(item => format(item))
    // 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
    const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
    // sourcesContent 中包含了各个文件的未压缩前的源码,根据文件名找出对应的源码
    const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
    return {
        file: originalInfo.source,
        content: originalFileContent,
        line: originalInfo.line,
        column: originalInfo.column,
        msg: error.msg,
        error: error.error
    }
}
function format(item) {
    return item.replace(/(\.\/)*/g, '')
}
function getMapFileContent(url) {
    return fs.readFileSync(path.resolve(__dirname, `./maps/${url.split('/').pop()}.map`), 'utf-8')
}

每次项目打包时,如果开启了 sourcemap,那么每一个 js 文件都会有一个对应的 map 文件。

bundle.js

bundle.js.map

这时 js 文件放在静态服务器上供用户访问,map 文件存储在服务器,用于还原错误信息。source-map 库可以根据压缩过的代码报错信息还原出未压缩前的代码报错信息。例如压缩后报错位置为 1 行 47 列,还原后真正的位置可能为 4 行 10 列。除了位置信息,还可以获取到源码原文。

上图就是一个代码报错还原后的示例。鉴于这部分内容不属于 SDK 的范围,所以我另开了一个 仓库 来做这个事,有兴趣可以看看。

Vue 错误

利用 window.onerror 是捕获不到 Vue 错误的,它需要使用 Vue 提供的 API 进行监听。

Vue.config.errorHandler = (err, vm, info) => {
    // 将报错信息打印到控制台
    console.error(err)
    lazyReportCache({
        info,
        error: err.stack,
        subType: 'vue',
        type: 'error',
        startTime: performance.now(),
        pageURL: getPageURL(),
    })
}

行为数据采集

PV、UV

PV(page view) 是页面浏览量,UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次,UV 同一天内多次访问只算一次。

对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 UV。

export default function pv() {
    lazyReportCache({
        type: 'behavior',
        subType: 'pv',
        startTime: performance.now(),
        pageURL: getPageURL(),
        referrer: document.referrer,
        uuid: getUUID(),
    })
}

页面停留时长

用户进入页面记录一个初始时间,用户离开页面时用当前时间减去初始时间,就是用户停留时长。这个计算逻辑可以放在 beforeunload 事件里做。

export default function pageAccessDuration() {
    onBeforeunload(() => {
        report({
            type: 'behavior',
            subType: 'page-access-duration',
            startTime: performance.now(),
            pageURL: getPageURL(),
            uuid: getUUID(),
        }, true)
    })
}

页面访问深度

记录页面访问深度是很有用的,例如不同的活动页面 a 和 b。a 平均访问深度只有 50%,b 平均访问深度有 80%,说明 b 更受用户喜欢,根据这一点可以有针对性的修改 a 活动页面。

除此之外还可以利用访问深度以及停留时长来鉴别电商刷单。例如有人进来页面后一下就把页面拉到底部然后等待一段时间后购买,有人是慢慢的往下滚动页面,最后再购买。虽然他们在页面的停留时间一样,但明显第一个人更像是刷单的。

页面访问深度计算过程稍微复杂一点:

  1. 用户进入页面时,记录当前时间、scrollTop 值、页面可视高度、页面总高度。
  2. 用户滚动页面的那一刻,会触发 scroll 事件,在回调函数中用第一点得到的数据算出页面访问深度和停留时长。
  3. 当用户滚动页面到某一点时,停下继续观看页面。这时记录当前时间、scrollTop 值、页面可视高度、页面总高度。
  4. 重复第二点...

具体代码请看:

let timer
let startTime = 0
let hasReport = false
let pageHeight = 0
let scrollTop = 0
let viewportHeight = 0
export default function pageAccessHeight() {
    window.addEventListener('scroll', onScroll)
    onBeforeunload(() => {
        const now = performance.now()
        report({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        }, true)
    })
    // 页面加载完成后初始化记录当前访问高度、时间
    executeAfterLoad(() => {
        startTime = performance.now()
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight
    })
}
function onScroll() {
    clearTimeout(timer)
    const now = performance.now()
    if (!hasReport) {
        hasReport = true
        lazyReportCache({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        })
    }
    timer = setTimeout(() => {
        hasReport = false
        startTime = now
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight        
    }, 500)
}
function toPercent(val) {
    if (val >= 1) return '100%'
    return (val * 100).toFixed(2) + '%'
}

用户点击

利用 addEventListener() 监听 mousedowntouchstart 事件,我们可以收集用户每一次点击区域的大小,点击坐标在整个页面中的具体位置,点击元素的内容等信息。

export default function onClick() {
    ['mousedown', 'touchstart'].forEach(eventType => {
        let timer
        window.addEventListener(eventType, event => {
            clearTimeout(timer)
            timer = setTimeout(() => {
                const target = event.target
                const { top, left } = target.getBoundingClientRect()
                lazyReportCache({
                    top,
                    left,
                    eventType,
                    pageHeight: document.documentElement.scrollHeight || document.body.scrollHeight,
                    scrollTop: document.documentElement.scrollTop || document.body.scrollTop,
                    type: 'behavior',
                    subType: 'click',
                    target: target.tagName,
                    paths: event.path?.map(item => item.tagName).filter(Boolean),
                    startTime: event.timeStamp,
                    pageURL: getPageURL(),
                    outerHTML: target.outerHTML,
                    innerHTML: target.innerHTML,
                    width: target.offsetWidth,
                    height: target.offsetHeight,
                    viewport: {
                        width: window.innerWidth,
                        height: window.innerHeight,
                    },
                    uuid: getUUID(),
                })
            }, 500)
        })
    })
}

页面跳转

利用 addEventListener() 监听 popstatehashchange 页面跳转事件。需要注意的是调用history.pushState()history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)。同理,hashchange 也一样。

export default function pageChange() {
    let from = ''
    window.addEventListener('popstate', () => {
        const to = getPageURL()
        lazyReportCache({
            from,
            to,
            type: 'behavior',
            subType: 'popstate',
            startTime: performance.now(),
            uuid: getUUID(),
        })
        from = to
    }, true)
    let oldURL = ''
    window.addEventListener('hashchange', event => {
        const newURL = event.newURL
        lazyReportCache({
            from: oldURL,
            to: newURL,
            type: 'behavior',
            subType: 'hashchange',
            startTime: performance.now(),
            uuid: getUUID(),
        })
        oldURL = newURL
    }, true)
}

Vue 路由变更

Vue 可以利用 router.beforeEach 钩子进行路由变更的监听。

export default function onVueRouter(router) {
    router.beforeEach((to, from, next) => {
        // 首次加载页面不用统计
        if (!from.name) {
            return next()
        }
        const data = {
            params: to.params,
            query: to.query,
        }
        lazyReportCache({
            data,
            name: to.name || to.path,
            type: 'behavior',
            subType: ['vue-router-change', 'pv'],
            startTime: performance.now(),
            from: from.fullPath,
            to: to.fullPath,
            uuid: getUUID(),
        })
        next()
    })
}

数据上报

上报方法

数据上报可以使用以下几种方式:

我写的简易 SDK 采用的是第一、第二种方式相结合的方式进行上报。利用 sendBeacon 来进行上报的优势非常明显。

使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报。一个 HTTP 请求包含发送和接收两个步骤。其实对于上报来说,我们只要确保能发出去就可以了。也就是发送成功了就行,接不接收响应无所谓。为此,我做了个实验,在 beforeunload 用 XMLHttpRequest 传送了 30kb 的数据(一般的待上报数据很少会有这么大),换了不同的浏览器,都可以成功发出去。当然,这和硬件性能、网络状态也是有关联的。

上报时机

上报时机有三种:

  1. 采用 requestIdleCallback/setTimeout 延时上报。
  2. 在 beforeunload 回调函数里上报。
  3. 缓存上报数据,达到一定数量后再上报。

建议将三种方式结合一起上报:

  1. 先缓存上报数据,缓存到一定数量后,利用 requestIdleCallback/setTimeout 延时上报。
  2. 在页面离开时统一将未上报的数据进行上报。

总结

仅看理论知识是比较难以理解的,为此我结合本文所讲的技术要点写了一个简单的监控 SDK,可以用它来写一些简单的 DEMO,帮助加深理解。再结合本文一起阅读,效果更好。

参考资料

性能监控

错误监控

行为监控

相关实践学习
通过轻量消息队列(原MNS)主题HTTP订阅+ARMS实现自定义数据多渠道告警
本场景将自定义告警信息同时分发至多个通知渠道的需求,例如短信、电子邮件及钉钉群组等。通过采用轻量消息队列(原 MNS)的主题模型的HTTP订阅方式,并结合应用实时监控服务提供的自定义集成能力,使得您能够以简便的配置方式实现上述多渠道同步通知的功能。
目录
相关文章
|
1月前
|
前端开发 算法 Java
【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
Flex 布局 布局的传统解决方案,基于盒状模型,依赖 display 属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。 2009年,W3C 提出了一种新的方案----Flex 布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。 一、Flex 布局是什么? Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
230 1
|
1月前
|
前端开发 算法 Java
【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
盒模型 盒模型: 所谓盒模型,就是浏览器为页面中的每个HTML元素生成的矩形盒子。 这些盒子们都要按照可见板式模型在页面上排布。 可见的板式模型主要由三个属性控制:position 属性、display 属性和 float属性。 position属性控制页面上元素间的位置关系。 display属性控制元素是堆叠、并排或者不在页面上显示。 float属性提供控制的方法,以便于把元素组成成多栏布局。 盒模型讲解: 在默认的情况下,每个盒子的边框是不可见的,背景也是透明的。 所以我们 不能直接的看到页面中的盒
148 1
|
7月前
|
数据采集 搜索推荐 前端开发
鸿蒙HarmonyOS埋点SDK,ClkLog适配鸿蒙埋点分析
ClkLog鸿蒙埋点SDK通过手动埋点的方式实现HarmonyOS 原生应用的前端数据采集。快速接入即可获取埋点数据,同时支持分析功能(基础统计分析、自定义分析、用户画像等)。
329 59
|
1月前
|
前端开发 算法 Java
【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
Animation属性 css3为Animation动画提供的几个属性如下: 属性名 属性值 animation-name 指定动画名称,该属性指定一个已有的关键帧定义。 animation-duration 指定动画持续时间。 animation-timing-funtion 指定动画变化速度。 animation-delay 指定动画延迟多长时间才开始执行。 animation-iteration-count 指定动画的循环执行次数。 animation:这是一个复合属性。
167 1
|
1月前
|
前端开发 算法 Java
【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
position定位(核心) 我们讲盒模型的时候,提到了3个属性可以用来控制页面排版。 三大属性:position属性,display属性,float属性。 position 属性控制页面上元素间的位置关系。 display 属性控制页面元素是否显示或者是堆叠还是并排显示。 float 属性提供控制方法。 通过float这种控制方法,可以实现多栏布局,导航菜单等等。 position属性是干嘛用的?怎么用?有哪些属性值? position属性控制页面上元素间的位置关系,也就是排版。 怎么用?要知道怎么用
317 0
|
7月前
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
486 70
|
9月前
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
592 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
521 10
|
6月前
|
开发框架 人工智能 大数据
HarmonyOS Next~HarmonyOS SDK应用服务:开发者的全新技术生态
本文深入解析了HarmonyOS SDK应用服务的技术架构与开发优势,涵盖其分布式服务框架、核心特性和开发流程。HarmonyOS凭借统一开发框架、高效跨设备协同及低延迟系统服务,为开发者提供全新技术生态。未来,随着生态完善,SDK将在智能设备场景支持、AI与大数据集成等方面持续演进,助力开发者释放创新潜力。
350 0
|
9月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
627 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex

热门文章

最新文章

下一篇
oss云网关配置