前端性能监控系统埋点指标即上报方法记录 | 青训营笔记

218 阅读3分钟

文章第一句话为“这是我参与「第四届青训营 」笔记创作活动的第3天

本文主要记录前端性能监控系统中常见的监控指标的监控方式和日志上报方法。

前端性能监控系统中一般会实现一个sdk用于开发者使用,该sdk主要就是进行了前端埋点监控,对前端事件追踪(Event Tracking),捕获、处理和发送的相关技术及实施过程。埋点是产品数据分析的基础,一般用于推荐系统的反馈、用户行为的监控和分析、新功能或者运营活动效果的统计分析等。

监控指标

根据本次项目要求,我们的sdk主要监控如下指标:

  • 异常监控:js异常、资源异常、白屏异常
  • 性能监控:FP、FCP、DOM Ready、DNS等
  • 用户行为数据,如:PV、页面停留时间staytime
  • HTTP请求监控,包括:请求链路、接口异常、返回信息等

异常监控

  • 普通的js异常:采用对于全局error添加监听
  • 资源异常:也是采用error,通过event.target上的属性进行区分
  • 白屏异常:由js错误引发页面白屏
window.addEventListener(
    "error",
    (event) => {
        if (event.target && (event.target.src || event.target.href)) {
           // errorType: "resourceError"
        } else {
           // errorType: "jsError",
           
           // 白屏检测
           let score = traverseEl(document.body, 1, false);
           if (score < 50) {
               sendWhiteScreeNode(log);
           }
        }
    },
    true
);
// 白屏检测函数,累计页面可见元素得分
export default function traverseEl(element, layer, identify) {
    const height = window.innerHeight || 0;
    let score = 0;
    const tagName = element.tagName;
    const isCal = tagName !== 'SCRIPT' && tagName !== 'STYLE' && tagName !== 'META' && tagName !== 'HEAD';
    if (isCal) {
        const len = element.children ? element.children.length : 0;
        if (len > 0) {
            for (let children = element.children, i = len - 1; i >= 0; i--) {
                score += traverseEl(children[i], layer + 1, score > 0); // 迭代累加得分
            }
        }
        if (score <= 0 && !identify) {
            // 元素在视口外,不计算得分
            if (
                element.getBoundingClientRect &&
                element.getBoundingClientRect().top >= height
            ) {
                return 0;
            }
        }
        score += 1 + 0.5 * layer;
    }
    return score;
}
  • 异步js异常:监听全局的unhandledrejection事件
window.addEventListener(
    "unhandledrejection",
    (event) => {
       // errorType: "promiseError",

       // 白屏检测
       let score = traverseEl(document.body, 1, false);
       if (score < 50) {
           sendWhiteScreeNode(log);
       }
},
    true
);

性能数据

paint指标

指标名描述实现方式
FP页面首次绘制时间performance.getEntriesByName("first-paint")[0]
FCP页面首次有内容绘制的时间performance.getEntriesByName('first-contentful-paint')[0]
LCP最大内容绘制事件new PerformanceObserver(...).observe({ type: 'largest-contentful-paint', buffered: true })
CLS累计位移偏移new PerformanceObserver(...).observe({ type: 'layout-shift', buffered: true })
FID页面加载阶段,用户首次交互操作的延时时间new PerformanceObserver(...).observe({ type: 'first-input', buffered: true })

timming指标

指标名描述实现方式 performance.timing
DNSLookup页面首次绘制时间domainLookupEnd - domainLookupStart
TCPConnectTCP连接时间connectEnd - connectStart
SSLConnectssl时间connectEnd - secureConnectionStart
requestCost请求耗时responseEnd - requestStart
responseCost返回耗时responseEnd - responseStart
DOMReadyDOM准备时间domComplete - domInteractive

HTTP请求监控

http请求监控主要是实现了xhr监控和fetch,但此处我有个疑问,如axios这样的ajax请求库,它好像是做了封装,xhr监控并不能捕获到它的异常,其出错时是promise错误,这种封装库的请求要怎么监控成http而不是js呢,总不能每种库都单独适配吧

XHR

实现方式主要是重写XMLHttpRequest的部分api

let XMLHttpRequest = window.XMLHttpRequest;
let oldSend = XMLHttpRequest.prototype.send;
let oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
    if (url.indexOf(tracker.logUrl) === -1) {
            this.logData = { method, url: url.split("?")[0] };
    }
    return oldOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
    if (this.logData) {
        let startTime = Date.now();
        let handler = (type) => () => {
            // 发送日志
        };
        addEvent(this, "load", handler("load"), false);
        addEvent(this, "error", handler("error"), false);
        addEvent(this, "abort", handler("abort"), false);
    }
    return oldSend.apply(this, arguments);
};

fetch

大同小异重写fetch函数

let fetch = window.fetch;
if (!fetch) return;
let newFetch = function (...args) {
    let startTime = Date.now();
    let method = args[2] && args[2].method ? args[2].method : "get";
    return fetch(...args).then((data) => {
       // 发送日志
       return Promise.resolve(data);
    });
};
window.fetch = newFetch;

数据上报

在这个场景中,需要考虑两个问题:

  • 如果数据上报接口与业务系统使用同一域名,浏览器对请求并发量有限制,所以存在网络资源竞争的可能性。(因为我们这是一个mini的学习版监控系统,这点就没辙...实际中要让日志上报和业务处在不同的域名下)
  • 浏览器通常在页面卸载时会忽略异步ajax请求,如果需要必须进行数据请求,一般在unload或者beforeunload事件中创建同步ajax请求,以此延迟页面卸载。从用户侧角度,就是页面跳转变慢。 常用的数据上报方式有Beacon Apiimage

sendBeacon

Beacon 接口用来调度向 Web 服务器发送的异步非阻塞请求。

  • Beacon 请求使用 HTTP POST方法,并且不需要有响应。
  • Beacon 请求能确保在页面触发 unload 之前完成初始化。
navigator.sendBeacon(url, data);

image

sendBeacon的兼容性问题是不可避免的,不过可以充分利用大部分浏览器会在页面卸载前完成图片的加载的特性,通过在页面动态添加img的方式上报数据。 由于img图片为get请求方式,不同服务器针对uri的长度有限制,长度超过限制时会出现HTTP 414错误,所以还要注意上报频率,减少一次性上传的属性过多。

兼容两种方式的tracker

import getUUID from "./getUUID";
function sendByBeacon(logUrl, params) {
    navigator.sendBeacon(logUrl, JSON.stringify(params));
}

function sendByImg(logUrl, params) {
    var img = new Image();
    img.onload = img.onerror = function () {
        img.onload = img.onerror = null;
        img = null;
    };
    img.src = `${logUrl}?params=${JSON.stringify(params)}`
}
function Tracker() {
    this.logUrl = null;
    this.send = function (params) {
        params.uuid = getUUID();
        if (navigator.sendBeacon) {
            sendByBeacon(this.logUrl, params)
        } else {
            sendByImg(this.logUrl, params)
        }
    };
    this.setUrl = (url) => {
        this.logUrl = url;
    }
}
export default new Tracker();

以上