前端工程化4:从0到1构建完整的前端监控平台

news2024/9/23 11:57:38

前言

一套完整的前端监控系统的主要部分:

  • 数据上报方式
  • 数据上送时机
  • 性能数据采集
  • 错误数据采集
  • 用户行为采集
  • 定制化指标
  • 监控sdk
    在这里插入图片描述
    监控的目的:
    在这里插入图片描述

一、数据上报方式

本文的方案是,优先navigator.sendBeacon,降级使用1x1像素gif图片,根据实际情况需要采用xhr/fetch。

1、图片打点

图片打点上报的优势:
1)支持跨域,一般而言,上报域名都不是当前域名,上报的接口请求会构成跨域
2)体积小且不需要插入dom中(相比之下,script、link要挂载到页面上才可以请求)
3)不需要等待服务器返回数据

图片打点缺点是:

1)url受浏览器长度限制

2)只能发送GET请求,无法获取相应结果

通过创建一个Image对象,将要上报的数据作为URL参数拼接到一个1x1像素的透明图片URL中,发送一个GET请求来触发上报。

const data = { event: 'click', element: 'button' };
const url = ` https://example.com/track?data= ${encodeURIComponent(JSON.stringify(data))}`;
const img = new Image();
img.src = url;

2、fetch请求上报

这类方法用于业务数据确实,模块未展示,需要紧急上报的情况

优点:可以灵活地设置请求头属性,post请求可以发送大体量数据,满足特定场景的埋点需求。

缺点:数据量大的请求占用带宽资源多,增加服务器压力。页面销毁时的监控埋点大概率上报失败。

3、sendBeacon

navigator.sendBeacon是一个用于发送少量数据到服务器的浏览器API。它有以下几个优点

  • 异步和非阻塞

    navigator.sendBeacon 是异步的,它不会阻塞浏览器的其他操作。这对于性能监控来说非常重要,因为都不希望监控的过程影响到页面的性能。

  • 在页面卸载时仍然可以发送数据

    当用户离开页面(例如关闭页面或者导航到其他页面)时,navigator.sendBeacon仍然可以发送数据。这对于捕获和上报页面卸载前的最后一些性能数据来说非常有用。

  • 低优先级

    navigator.sendBeacon 发送的请求是低优先级的,它不会影响到页面的其他网络请求。

  • 简单易用

    navigator.sendBeacon 的API非常简单,只需要提供上报的URL和数据,就可以发送请求。

与此同时,navigator.sendBeacon 也有一些限制。例如,它只能发送POST请求,不能发送GET请求。而且,它发送的请求没有返回值,不能接收服务器的响应。

最后,一些旧的浏览器可能不支持 navigator.sendBeacon。因此,在使用 navigator.sendBeacon 时,需要根据实际情况进行兼容性处理。

navigator.sendBeacon()可以在用户离开页面(包括关闭浏览器窗口、标签页,或者在移动设备上切换应用等情况)时进行数据上报。

navigator.sendBeacon()的设计目的是在页面卸载时异步发送数据,尽可能确保数据能够被发送出去而不影响页面的卸载流程。然而,当应用被强制终止时,操作系统可能会立即终止应用的所有进程,不给应用任何机会执行数据上报操作。

以下是一个使用navigator.sendBeacon()的示例代码:

function onPageUnload() {
  const data = { /* 要上报的数据 */ };
  const url = '/data-collection-endpoint';
  navigator.sendBeacon(url, JSON.stringify(data));
}

window.addEventListener('unload', onPageUnload);

由于兼容问题,一些旧的浏览器可能不支持 navigator.sendBeacon。因此,在使用 navigator.sendBeacon 时,需要根据实际情况进行兼容性处理:

import {isSupportSendBeacon} from './util'
 
 
// 如果浏览器不支持 sendBeacon,就使用图片打点
const sendBeacon = (function(){
    if(isSupportSendBeacon()){
      return window.navigator.sendBeacon.bind(window.navigator)
    }
    const reportImageBeacon = function(url, data){
        reportImage(url, data)
    }
    return reportImageBeacon
})()
 
export function reportImage(url, data) {
    const img = new Image();
    img.src = url + '?reportData=' + encodeURIComponent(JSON.stringify(data));
}

三、数据上报时机

1、上报时机有三种:

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

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

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

2、还有一种情况:

用户杀掉app进程时

将数据暂存在localStorage中,在用户下次进入应用时检查并上报是一种可行的方法,业内也有这样做的情况。

以下是一个示例代码实现:

// 上报数据的函数
function reportData(data) {
  // 发送数据到服务器的逻辑,这里只是示例
  fetch('/reporting-endpoint', {
    method: 'POST',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json',
    },
  })
   .then(response => response.json())
   .then(result => {
      console.log('数据上报成功:', result);
      // 上报成功后可以从 localStorage 中删除数据
      localStorage.removeItem('pendingData');
    })
   .catch(error => {
      console.error('数据上报失败:', error);
      // 上报失败可以考虑再次尝试或者在下次进入时继续尝试
    });
}

// 在合适的时机尝试上报 localStorage 中的数据
function checkAndReportPendingData() {
  const pendingData = localStorage.getItem('pendingData');
  if (pendingData) {
    reportData(JSON.parse(pendingData));
  }
}

// 在某个事件触发时(比如页面加载)调用检查函数
window.addEventListener('load', checkAndReportPendingData);

// 在需要上报数据时,先将数据存入 localStorage
function saveDataForLaterReporting(data) {
  localStorage.setItem('pendingData', JSON.stringify(data));
}

这种方法的优点是可以在一定程度上提高数据上报的成功率,尤其是对于一些关键数据,即使在用户意外退出应用的情况下也有机会在下次进入时上报。

然而,这种方法也有一些局限性:

  1. 如果用户长时间不使用应用或者清除了浏览器缓存(包括localStorage数据),那么数据可能无法上报。
  2. 如果数据量较大,存储在localStorage中可能会占用较多的存储空间。
  3. 需要考虑数据的安全性和隐私性,确保存储在localStorage中的数据不会被恶意获取。

四、性能数据收集上报

以Spa页面来说,页面的加载过程大致是这样的:
在这里插入图片描述

包括dns查询、建立tcp连接、发送http请求、返回html文档、html文档解析等阶段

最初,可以通过 window.performance.timing 来获取加载过程模型中各个阶段的耗时数据,后来 window.performance.timing 被废弃,通过 PerformanceObserver 来获取。旧的 api,返回的是一个 UNIX 类型的绝对时间,和用户的系统时间相关,分析的时候需要再次计算。而新的 api,返回的是一个相对时间,可以直接用来分析。
根据最初的规划,性能监控需要收集的数据指标需要有FP、FCP、LCP、DOMContentLoaded、onload、资源加载时间、接口请求时间。

收集FP、FCP、LCP、资源加载时间具体是利用浏览器Performance API。关于Performance API:

// window.performance.timing 各字段说明
{
    navigationStart,  // 同一个浏览器上下文中,上一个文档结束时的时间戳。如果没有上一个文档,这个值会和 fetchStart 相同。
    unloadEventStart,  // 上一个文档 unload 事件触发时的时间戳。如果没有上一个文档,为 0。
    unloadEventEnd, // 上一个文档 unload 事件结束时的时间戳。如果没有上一个文档,为 0。
    redirectStart, // 表示第一个 http 重定向开始时的时间戳。如果没有重定向或者有一个非同源的重定向,为 0。
    redirectEnd, // 表示最后一个 http 重定向结束时的时间戳。如果没有重定向或者有一个非同源的重定向,为 0。
    fetchStart, // 表示浏览器准备好使用 http 请求来获取文档的时间戳。这个时间点会在检查任何缓存之前。
    domainLookupStart, // 域名查询开始的时间戳。如果使用了持久连接或者本地有缓存,这个值会和 fetchStart 相同。
    domainLookupEnd, // 域名查询结束的时间戳。如果使用了持久连接或者本地有缓存,这个值会和 fetchStart 相同。
    connectStart, // http 请求向服务器发送连接请求时的时间戳。如果使用了持久连接,这个值会和 fetchStart 相同。
    connectEnd, // 浏览器和服务器之前建立连接的时间戳,所有握手和认证过程全部结束。如果使用了持久连接,这个值会和 fetchStart 相同。
    secureConnectionStart, // 浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,返回 0。
    requestStart, // 浏览器向服务器发起 http 请求(或者读取本地缓存)时的时间戳,即获取 html 文档。
    responseStart, // 浏览器从服务器接收到第一个字节时的时间戳。
    responseEnd, // 浏览器从服务器接受到最后一个字节时的时间戳。
    domLoading, // dom 结构开始解析的时间戳,document.readyState 的值为 loading。
    domInteractive, // dom 结构解析结束,开始加载内嵌资源的时间戳,document.readyState 的状态为 interactive。
    domContentLoadedEventStart, // DOMContentLoaded 事件触发时的时间戳,所有需要执行的脚本执行完毕。
    domContentLoadedEventEnd,  // DOMContentLoaded 事件结束时的时间戳
    domComplete, // dom 文档完成解析的时间戳, document.readyState 的值为 complete。
    loadEventStart, // load 事件触发的时间。
    loadEventEnd // load 时间结束时的时间。
}

1、收集上报FP;

FP(First Paint)首次绘制,即浏览器开始绘制页面的时间点。这包括了任何用户自定义的绘制,它是渲染任何文本、图像、SVG等的开始时间

import { getPageURL, isSupportPerformanceObserver } from '../utils/util'
import { lazyReportCache } from '../utils/report'

export default function observePaint() {
if (!isSupportPerformanceObserver()) return

const entryHandler = (list) => {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-paint') {
            observer.disconnect()
        }

        const json = entry.toJSON()
        delete json.duration

        const reportData = {
            ...json,
            subType: entry.name,
            type: 'performance',
            pageURL: getPageURL(),
        }

        lazyReportCache(reportData)
    }
}

const observer = new PerformanceObserver(entryHandler)
// buffered 属性表示是否观察缓存数据,也就是说观察代码添加时机比事情触发时机晚也没关系。
observer.observe({ type: 'paint', buffered: true })
}

代码中observer.disconnect()是PerformanceObserver对象的一个方法,用于停止观察性能指标并断开与回调函数的连接。

事实上

observer.observe({ type: 'paint', buffered: true })

包含两种性能指标:first-contentful-paintfirst-paint

当调用observer.disconnect()方法时,PerformanceObserver对象将停止观察性能指标,并且不再接收任何性能指标的更新。与此同时,与回调函数的连接也会被断开,即使有新的性能指标数据产生,也不会再触发回调函数。

这个方法通常在不再需要观察性能指标时调用,以避免不必要的资源消耗。

2、收集上报FCP

FCP(First Contentful Paint):首次内容绘制,即浏览器首次绘制DOM内容的时间点,如文本、图像、SVG等。

看起来FCP和FP一致,其实还是有区别的

FCP(First Contentful Paint):FCP是指页面上首次渲染任何文本、图像、非空白的canvas或SVG的时间点。它表示了用户首次看到页面有实际内容的时间,即页面开始呈现有意义的内容的时间点。

FP(First Paint):FP是指页面上首次渲染任何内容的时间点,包括背景颜色、图片、文本等。它表示了页面开始呈现任何可视化内容的时间,但不一定是有意义的内容。

简而言之,FCP关注的是页面上首次呈现有意义内容的时间点,而FP关注的是页面上首次呈现任何可视化内容的时间点。FCP更关注用户感知的页面加载时间,因为它表示用户可以开始阅读或与页面进行交互的时间点。而FP则更关注页面开始渲染的时间点,无论内容是否有意义

import { getPageURL, isSupportPerformanceObserver } from '../utils/util'
import { lazyReportCache } from '../utils/report'

export default function observePaint() {
if (!isSupportPerformanceObserver()) return

    const entryHandler = (list) => {        
        for (const entry of list.getEntries()) {
            if (entry.name === 'first-contentful-paint') {
                observer.disconnect()
            }

            const json = entry.toJSON()
            delete json.duration

            const reportData = {
                ...json,
                subType: entry.name,
                type: 'performance',
                pageURL: getPageURL(),
            }

            lazyReportCache(reportData)
        }
    }

    const observer = new PerformanceObserver(entryHandler)
    // buffered 属性表示是否观察缓存数据,也就是说观察代码添加时机比事情触发时机晚也没关系。

    observer.observe({ type: 'paint', buffered: true })

}

3、收集上报LCP

LCP(Largest Contentful Paint):最大内容绘制,即视口中最大的图像或文本块的渲染完成的时间点

import { getPageURL, isSupportPerformanceObserver } from '../utils/util'
import { lazyReportCache } from '../utils/report'

export default function observeLCP() {
if (!isSupportPerformanceObserver()) {
	return
}

    const entryHandler = (list) => {

        if (observer) {
            observer.disconnect()
        }
        
        for (const entry of list.getEntries()) {
            const json = entry.toJSON()
            delete json.duration

            const reportData = {
                ...json,
                target: entry.element?.tagName,
                name: entry.entryType,
                subType: entry.entryType,
                type: 'performance',
                pageURL: getPageURL(),
            }
            
            lazyReportCache(reportData)
        }
    }

    const observer = new PerformanceObserver(entryHandler)
    observer.observe({ type: 'largest-contentful-paint', buffered: true })

}

4、收集上报DOMContentLoaded

DOMContentLoaded:当HTML文档被完全加载和解析完成后,DOMContentLoaded事件被触发,无需等待样式表、图像和子框架的完成加载

import { lazyReportCache } from '../utils/report'

export default function observerLoad() {
	['DOMContentLoaded'].forEach(type => onEvent(type))
}

function onEvent(type) {
	function callback() {
		lazyReportCache({
		type: 'performance',
		subType: type.toLocaleLowerCase(),
		startTime: performance.now(),
		})	
	        window.removeEventListener(type, callback, true)
	    }
    window.addEventListener(type, callback, true)
}

5、收集上报onload数据

onload:当所有需要立即加载的资源(如图片和样式表)已加载完成时的时间点

import { lazyReportCache } from '../utils/report'

export default function observerLoad() {
	['load'].forEach(type => onEvent(type))
}

function onEvent(type) {
function callback() {
	lazyReportCache({
		type: 'performance',
		subType: type.toLocaleLowerCase(),
		startTime: performance.now(),
	})
        window.removeEventListener(type, callback, true)
    }
    window.addEventListener(type, callback, true)
}

6、收集上报资源加载时间

收集资源加载时间

observer.observe({ type: 'resource', buffered: true })

我在想什么是资源加载时间?应该就是下面的entry.duration的。我觉得写监控SDK很有意义,可以更加深入的学习浏览器模型。了解浏览器是怎么看待各种html文件资源的

import { executeAfterLoad, isSupportPerformanceObserver} from '../utils/util'
import { lazyReportCache } from '../utils/report'

export default function observeEntries() {
	executeAfterLoad(() => {
	observeEvent('resource')
	})
}

export function observeEvent(entryType) {
function entryHandler(list) {
	const data = list.getEntries()
	for (const entry of data) {
		if (observer) {
			observer.disconnect()
		}
	    lazyReportCache({
	        name: entry.name, // 资源名称
	        subType: entryType,
	        type: 'performance',
	        sourceType: entry.initiatorType, // 资源类型
	        duration: entry.duration, // 资源加载耗时
	        dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 耗时
	        tcp: entry.connectEnd - entry.connectStart, // 建立 tcp 连接耗时
	        redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
	        ttfb: entry.responseStart, // 首字节时间
	        protocol: entry.nextHopProtocol, // 请求协议
	        responseBodySize: entry.encodedBodySize, // 响应内容大小
	        responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头部大小
	        resourceSize: entry.decodedBodySize, // 资源解压后的大小
	        startTime: performance.now(),
	    })
       }
    }

    let observer
    if (isSupportPerformanceObserver()) {
        observer = new PerformanceObserver(entryHandler)
        observer.observe({ type: entryType, buffered: true })
    }

}

7、收集上报接口请求时间

这里通过覆写原生xhr对象方法,对方法做拦截实现接口时间收集以及上报

import { originalOpen, originalSend, originalProto } from '../utils/xhr'
import { lazyReportCache } from '../utils/report'

function overwriteOpenAndSend() {
	originalProto.open = function newOpen(...args) {
		this.url = args[1]
		this.method = args[0]
		originalOpen.apply(this, args)
	}

    originalProto.send = function newSend(...args) {
        this.startTime = Date.now()

        const onLoadend = () => {
            this.endTime = Date.now()
            this.duration = this.endTime - this.startTime
            const { status, duration, startTime, endTime, url, method } = this
            const reportData = {
                status,
                duration,
                startTime,
                endTime,
                url,
                method: (method || 'GET').toUpperCase(),
                success: status >= 200 && status < 300,
                subType: 'xhr',
                type: 'performance',
            }
            lazyReportCache(reportData)            
            this.removeEventListener('loadend', onLoadend, true)
        }
        this.addEventListener('loadend', onLoadend, true)
        originalSend.apply(this, args)
    }
}

export default function xhr() {
overwriteOpenAndSend()
}

五、错误数据收集上报

根据最初的规划需要收集资源加载错误js错误promise错误

1、收集上报资源加载错误

收集 JavaScript、CSS 和图片的加载错误,使用window.addEventListener监听错误

import { lazyReportCache } from '../utils/report'
import { getPageURL } from '../utils/util'

export default function 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)

}

2、收集上报js错误

收集 JavaScript 错误,可以使用 window.onerror 或者 window.addEventListener('error', callback)

import { lazyReportCache } from '../utils/report'
import { getPageURL } from '../utils/util'

export default function error() {

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

}

说明一下window.onerror无法捕获资源加载错误,所以这里可以单独拿来监听js错误。

3、收集上报promise错误

收集 Promise 错误,可以使用 window.addEventListener(‘unhandledrejection’, callback)

import { lazyReportCache } from '../utils/report'
import { getPageURL } from '../utils/util'

export default function error() {

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

}

为了减少对html文件代码的干扰,错误收集可以添加一个缓存代理。

六、行为数据收集上报

根据最初的规划,行为数据收集pv、uv,页面停留时长,用户点击。

1、收集上报pv、uv

收集 pv(Page View,页面浏览量)和 uv(Unique Visitor,独立访客)数据,需要在每次页面加载时发送一个请求到服务器,然后在服务器端进行统计

import { lazyReportCache } from '../utils/report'
import getUUID from './getUUID'
import { getPageURL } from '../utils/util'

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

这里只能收集了pv数据,uv数据统计需要在服务端进行。

2、页面上报停留时长

收集页面停留时长,可以在页面加载时记录一个开始时间,然后在页面卸载时记录一个结束时间,两者的差就是页面的停留时长。这个计算逻辑可以放在beforeunload事件里做

import { report } from '../utils/report'
import { onBeforeunload, getPageURL } from '../utils/util'
import getUUID from './getUUID'

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

3、用户点击上报

收集用户点击事件,可以使用 addEventListener 来监听 click 事件,这里借助了冒泡

import { lazyReportCache } from '../utils/report'
import { getPageURL } from '../utils/util'
import getUUID from './getUUID'

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

}

4、判断用户长时间停留在页面的方法

在前端埋点监控中,可以通过以下几种方式来分辨用户长时间停留在页面上:

一、使用计时器

  1. 设置定时器

    • 在页面加载时,可以启动一个定时器,每隔一段时间(比如 1 分钟)检查用户的活动状态。
    • 可以使用setInterval函数来设置定时器,例如:
      let timer;
      window.addEventListener('load', () => {
        timer = setInterval(checkUserActivity, 60000); // 每分钟检查一次
      });
      
  2. 检测用户活动

    • 可以通过监听用户的鼠标移动、键盘输入、滚动等事件来判断用户是否处于活动状态。当用户有这些活动时,重置定时器。
    • 例如:
      function checkUserActivity() {
        // 如果用户在一段时间内没有活动,可以认为用户长时间停留在页面上
        // 这里可以添加上报逻辑或其他处理
        console.log('用户可能长时间停留在页面上');
      }
      
      window.addEventListener('mousemove', resetTimer);
      window.addEventListener('keydown', resetTimer);
      window.addEventListener('scroll', resetTimer);
      
      function resetTimer() {
        clearInterval(timer);
        timer = setInterval(checkUserActivity, 60000);
      }
      

二、页面可见性 API

  1. 检测页面可见性
    • 使用Page Visibility API可以检测页面是否可见以及用户是否正在与页面进行交互。
    • 当页面变为不可见时,可以停止一些不必要的监控操作,当页面重新变为可见时,再恢复监控。
    • 例如:
      document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'visible') {
          // 页面可见,用户可能正在与页面交互
          resetTimer();
        } else {
          // 页面不可见,用户可能离开了页面
          clearInterval(timer);
        }
      });
      

三、心跳检测

  1. 发送心跳信号

    • 可以定期向服务器发送一个心跳信号,以表示用户仍然在与页面进行交互。如果一段时间内没有收到心跳信号,可以认为用户可能长时间停留在页面上或者已经离开了页面。
    • 例如,可以使用fetchXMLHttpRequest定期向服务器发送一个请求:
      let heartbeatInterval;
      window.addEventListener('load', () => {
        heartbeatInterval = setInterval(sendHeartbeat, 30000); // 每 30 秒发送一次心跳
      });
      
      function sendHeartbeat() {
        fetch('/heartbeat-endpoint', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
        });
      }
      
  2. 服务器端判断

    • 在服务器端,可以记录每个用户的最后心跳时间。如果一个用户的最后心跳时间与当前时间的差值超过一定阈值,可以认为用户可能长时间停留在页面上或者已经离开了页面。

通过结合以上方法,可以较为准确地判断用户是否长时间停留在页面上,并进行相应的监控和上报操作。需要注意的是,不同的方法可能适用于不同的场景,需要根据实际情况进行选择和调整。

七、改造完善四维监控类

性能数据、错误数据、行为数据入口文件的收集方法在监控类四维init方法内初始化

import performance from './performance/index'
import behavior from './behavior/index'
import error from './error/index'

class FourDimension {
	constructor() {
		this.init()
	}
	// 初始化
	init() {
		performance()
		error()
		behavior()
	}
}

new FourDimension().init()

在具体使用过程中,采用异步加载的方式引入。

八、自定义指标

1、long task

执行时间超过50ms的任务,被称为 long task 长任务

获取页面的长任务列表:

const entryHandler = list => {
  for (const long of list.getEntries()) {
    // 获取长任务详情
    console.log(long);
  }
};

let observer = new PerformanceObserver(entryHandler);
observer.observe({ entryTypes: ["longtask"] });

2、memory页面内存

performance.memory 可以显示此刻内存占用情况,它是一个动态值,其中:

  • jsHeapSizeLimit 该属性代表的含义是:内存大小的限制。

  • totalJSHeapSize 表示总内存的大小。

  • usedJSHeapSize 表示可使用的内存的大小。

通常,usedJSHeapSize 不能大于 totalJSHeapSize,如果大于,有可能出现了内存泄漏

// load事件中获取此时页面的内存大小
window.addEventListener("load", () => {
  console.log("memory", performance.memory);
});

3、首屏加载时间

首屏加载时间和首页加载时间不一样,首屏指的是屏幕内的dom渲染完成的时间
比如首页很长需要好几屏展示,这种情况下屏幕以外的元素不考虑在内
计算首屏加载时间流程
1)利用MutationObserver监听document对象,每当dom变化时触发该事件
2)判断监听的dom是否在首屏内,如果在首屏内,将该dom放到指定的数组中,记录下当前dom变化的时间点
3)在MutationObserver的callback函数中,通过防抖函数,监听document.readyState状态的变化
4)当document.readyState === 'complete'停止定时器和 取消对document的监听
5)遍历存放dom的数组,找出最后变化节点的时间,用该时间点减去performance.timing.navigationStart 得出首屏的加载时间

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div>Some content</div>
  <script>
    // 创建一个 MutationObserver 实例来监听 DOM 变化
    const observer = new MutationObserver((mutationsList, observer) => {
      mutationsList.forEach(mutation => {
        if (isInFirstScreen(mutation.target)) {
          addToFirstScreenElements(mutation.target, Date.now());
        }
      });
      debouncedCheckReadyState();
    });

    // 判断元素是否在首屏内的函数
    function isInFirstScreen(element) {
      const rect = element.getBoundingClientRect();
      return rect.top < window.innerHeight && rect.bottom > 0;
    }

    const firstScreenElements = [];
    function addToFirstScreenElements(element, time) {
      firstScreenElements.push({ element, time });
    }

    let timer;
    function debouncedCheckReadyState() {
      clearTimeout(timer);
      timer = setTimeout(() => {
        if (document.readyState === 'complete') {
          observer.disconnect();
          clearTimeout(timer);
          calculateFirstScreenLoadTime();
        }
      }, 100);
    }

    function calculateFirstScreenLoadTime() {
      let lastChangeTime = 0;
      for (const { time } of firstScreenElements) {
        if (time > lastChangeTime) {
          lastChangeTime = time;
        }
      }
      const loadTime = lastChangeTime - performance.timing.navigationStart;
      console.log(`首屏加载时间为:${loadTime} 毫秒`);
    }

    // 开始监听 document 对象的变化
    observer.observe(document, { childList: true, subtree: true });
  </script>
</body>

</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2157467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言 | Leetcode C语言题解之第419题棋盘上的战舰

题目&#xff1a; 题解&#xff1a; int countBattleships(char** board, int boardSize, int* boardColSize){int row boardSize;int col boardColSize[0];int ans 0;for (int i 0; i < row; i) {for (int j 0; j < col; j) {if (board[i][j] X) {if (i > 0 &…

手机在网状态查询接口如何用PHP进行调用?

一、什么是手机在网状态查询接口&#xff1f; 手机在网状态查询接口&#xff0c;即输入手机号码查询手机号在网状态&#xff0c;返回有正常使用、停机、在网但不可用、不在网&#xff08;销号/未启用/异常&#xff09;、预销户等多种状态。 二、手机在网状态查询适用哪些场景…

C++ | Leetcode C++题解之第429题N叉树的层序遍历

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<vector<int>> levelOrder(Node* root) {if (!root) {return {};}vector<vector<int>> ans;queue<Node*> q;q.push(root);while (!q.empty()) {int cnt q.size();vector<…

Comfyui海报工作流:出图快,质量高!

前言 工作流获取方式放在这里了 在快节奏的现代生活中&#xff0c;高效的工作流程对于企业和个人而言&#xff0c;无疑是提升竞争力的关键。 特别是在设计领域&#xff0c;能够快速而精准地完成海报设计&#xff0c;不仅意味着时间的节省&#xff0c;更代表着工作效率的飞跃。…

分治(快排系列)

一、基本思路 传统快排只是把数组分成两段进行排序&#xff0c;但是这样遇到重复数字多的数组就会超时&#xff0c;所以引入数组分三块&#xff1a; 流程&#xff1a; 1、初始化&#xff1a;l -1, r nums.size(); 2、先随机数选出 key 作为基准元素。srand(time(NULL)); in…

路径处理 | 关键点提取之Douglas–Peucker算法(附ROS C++/Python实现)

目录 0 专栏介绍1 路径关键点提取2 道格拉斯-普克算法Douglas–Peucker3 算法实现与可视化3.1 ROS C仿真3.2 Python仿真 0 专栏介绍 &#x1f525;课设、毕设、创新竞赛必备&#xff01;&#x1f525;本专栏涉及更高阶的运动规划算法轨迹优化实战&#xff0c;包括&#xff1a;…

深入理解并发原子性、可见性、有序性与JMM内存模型

1. 并发三大特性 并发编程Bug的源头&#xff1a;原子性、可见性和有序性问题 1.1 原子性 一个或多个操作&#xff0c;要么全部执行且在执行过程中不被任何因素打断&#xff0c;要么全部不执行。在 Java 中&#xff0c;对基本数据类型的变量的读取和赋值操作是原子性操作&…

HTB-GreenHorn 靶机笔记

GreenHorn 靶机笔记 概述 GreenHorn 是 HTB 上的一个 linux easy 难度的靶机&#xff0c;主要是通过信息搜集和代码审计找到对我们有用的信息。其中还包含了对pdf文件的修复技术 靶机地址&#xff1a;https://app.hackthebox.com/machines/GreenHorn 一丶 nmap 扫描 1&…

Spring Boot 学习之路 -- 基础认知

前言 最近因为业务需要&#xff0c;被拉去研究后端的项目&#xff0c;代码基于 Spring Boot&#xff0c;对我来说完全小白&#xff0c;需要重新学习研究…出于个人习惯&#xff0c;会以 Blog 文章的方式做一些记录&#xff0c;文章内容基本来源于「 Spring Boot 从入门到精通&…

Mac系统Docker中SQLserver数据库文件恢复记录

Mac系统Docker中SQLserver数据库文件恢复记录 Mac想要安装SQLsever&#xff0c;通过docker去拉去镜像是最简单方法。 一、下载Docker Docker 下载安装&#xff1a; 需要‘科学上网’ 才能访问到docker官网。&#xff08; https://docs.docker.com/desktop/install/mac-ins…

短剧APP分销小视频联盟收益源码带版权激励视频无需自己上传短剧

功能介绍&#xff1a; 带2000多部短剧资源&#xff0c;有版权&#xff0c;无需自己更新短剧&#xff0c; 已对接广告联盟&#xff0c;解锁短剧观看激励视频&#xff0c;对接各大广告平台 带刷小视频功能&#xff0c;插入视频广告&#xff0c;获取广告收益&#xff0c; 带任…

Stable Diffusion绘画 | ControlNet应用-instant-ID控制器:快速生成人物多角度图片

使用 instant-ID 控制器&#xff0c;用户只需要提供一张正脸图片&#xff0c;就可以快速地给人物生成多角度图片的&#xff0c;从而很好的保持了人物的一致性。 对于要制作小说推文、创建人物故事情节的创作&#xff0c;是一个非常好用且高效的功能。 准备工作 使用该控制类型&…

什么是端到端(end to end)大模型,它和传统的大模型有什么区别?其优势与劣势是什么?

“ 端到端模型&#xff0c;是一个直接由输入获取输出的过程 ” 最近有一个很火的关于人工智能模型的词——端到端模型。 那么什么是端到端模型&#xff1f;为什么会提出端到端模型&#xff0c;以及它解决了哪些问题&#xff1f; 今天我们就来一起了解一下这个端到端模型。 …

如何使用ChatGPT撰写文献综述?7个步骤轻松搞定

大家好,感谢关注。我是七哥,一个在高校里不务正业,折腾学术科研AI实操的学术人。关于使用ChatGPT等AI学术科研的相关问题可以和作者七哥(yida985)交流,多多交流,相互成就,共同进步,为大家带来最酷最有效的智能AI学术科研写作攻略。 撰写文献综述对于研究人员和学生来说…

ssm汉服文化平台网站

专业团队&#xff0c;咨询就送开题报告&#xff0c;欢迎大家咨询&#xff0c;联系方式在文章底部 摘 要 本论文主要论述了如何使用JAVA语言开发一个汉服文化平台网站 &#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象…

【题解】—— LeetCode一周小结38

&#x1f31f;欢迎来到 我的博客 —— 探索技术的无限可能&#xff01; &#x1f31f;博客的简介&#xff08;文章目录&#xff09; 【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结37 16.公交站间的距离 题目链接&#xff1a;1184. 公交站间的距…

【Linux】入门【更详细,带实操】

Linux全套讲解系列&#xff0c;参考视频-B站韩顺平&#xff0c;本文的讲解更为详细 目录 1、课程内容 2、应用领域 3、概述 4、 Linux和Unix 5、VMware15.5和CentOS7.6安装 6、网络连接三种方式 7、虚拟机克隆 8、虚拟机快照 9、虚拟机迁移删除 10、vmtools 11、目录…

抱歉占用公共资源,大家别猜啦,我们在一起了@Yaker

家人们上午好呀 这里是超绝脱单牛一枚 没错&#xff0c;我和Yaker有一个孩子&#xff08;bushi 今天我们的孩子YakLang来给大家介绍介绍&#xff0c;ta对块作用域的处理方式 在编程中&#xff0c;作用域&#xff08;Scope&#xff09;指的是变量、函数和对象的可访问性和生命…

文件查找和打包压缩【1.7】

文件查找和打包压缩【1.7】 八、文件查找和打包压缩8.1 文件查找8.1.1 locate8.1.2 findfind8.1.2.1 指定搜索目录层级8.1.2.2 先处理文件再处理目录8.1.2.3 根据文件名和inode查找8.1.2.4 根据属主属组查找8.1.2.5 根据文件类型查找8.1.2.6 空文件或目录8.1.2.7 组合条件8.1.2…

Vue项目之Element-UI(Breadcrumb)动态面包屑效果 el-breadcrumb

效果预览 需要导航的页面Vue.js 最笨的方法就是在每个需要面包屑的页面中固定写好 <template><div class="example-container"><el-breadcrumb separator="/"