如何去做前端监控

280 阅读3分钟

前端监控的范围

  1. 性能监控
    1. 基础数据(首次渲染时间,白屏等)
    2. 可交互时间
    3. 资源加载
  2. 异常监控
    1. js 异常
    2. ajax异常
    3. 异常文件

前端监控系统组成

  1. 数据采集
  2. 数据上报
  3. 数据可视化
  4. 监控告警

数据采集

  1. 根据 performance.timing 的时间,计算出各个阶段的时间
 getPerData(timing) {
        return {
            // 网络建连
            pervPage: filterTime(timing.fetchStart, timing.navigationStart), // 上一个页面
            redirect: filterTime(timing.responseEnd, timing.redirectStart), // 页面重定向时间
            dns: filterTime(timing.domainLookupEnd, timing.domainLookupStart), // DNS查找时间
            connect: filterTime(timing.connectEnd, timing.connectStart), // TCP建连时间
            network: filterTime(timing.connectEnd, timing.navigationStart), // 网络总耗时

            // 网络接收
            send: filterTime(timing.responseStart, timing.requestStart), // 前端从发送到接收到后端第一个返回
            receive: filterTime(timing.responseEnd, timing.responseStart), // 接受页面时间
            request: filterTime(timing.responseEnd, timing.requestStart), // 请求页面总时间

            // 前端渲染
            dom: filterTime(timing.domComplete, timing.domLoading), // dom解析时间
            loadEvent: filterTime(timing.loadEventEnd, timing.loadEventStart), // loadEvent时间
            frontend: filterTime(timing.loadEventEnd, timing.domLoading), // 前端总时间

            // 关键阶段
            load: filterTime(timing.loadEventEnd, timing.navigationStart), // 页面完全加载总时间
            domReady: filterTime(
                timing.domContentLoadedEventStart,
                timing.navigationStart
            ), // domready时间
            interactive: filterTime(
                timing.domInteractive,
                timing.navigationStart
            ), // 可操作时间
            ttfb: filterTime(timing.responseStart, timing.navigationStart) // 首字节时间
        };
    },
  1. 计算关键时间任务,
const perfObserver = new PerformanceObserver()
perfObserver.observe('paint')

观察 paint 时,返回格式
/*
 *   [{
 *         duration: 0
           entryType: "paint"
           name: "first-paint"
           startTime: 151.10000002384186
           first-paint: 223.69999998807907
 *   },{
 *       duration: 0
         entryType: "paint"
         name: "first-contentful-paint"
        startTime: 151.10000002384186
 *   
 *   }]
*/    

 getPaintTime() {
            const data  = {}
            getObserver('paint', entries => {
                entries.forEach(entry => {
                    data[entry.name] = entry.startTime
                })
            })
            return data
        }
  1. 资源加载的时间控制
 let entries = performance.getEntriesByType("resource");
            let entriesData = resolveEntries(entries)
            cb(entriesData)
  1. ajax的时间上报
  • 重写 xhr 的 open 和 send 方法
  • 监听 load error abort 方法
  • 对 fetch 方法的重写
let xhr = window.XMLHttpRequest;

    let _originOpen = xhr.prototype.open;
    xhr.prototype.open = function(method, url, async, user, password) {
        // 记录在里面
        this._eagle_xhr_info = {
            url: url,
            method: method,
            status: null
        };
        return _originOpen.apply(this, arguments); // 执行原有的 open 方法
    };


    let _originSend = xhr.prototype.send;
    xhr.prototype.send = function(value) {
        let _self = this;
        this._eagle_start_time = Date.now(); // 记录发送开始时间

        let ajaxEnd = event => () => {
            if (_self.response) {
                let responseSize = null;
                switch (_self.responseType) {
                    case "json":
                        responseSize = JSON && JSON.stringify(_this.response).length;
                        break;
                    case "blob":
                    case "moz-blob":
                        responseSize = _self.response.size;
                        break;
                    case "arraybuffer":
                        responseSize = _self.response.byteLength;
                        break;
                    case "document":
                        responseSize =
                            _self.response.documentElement &&
                            _self.response.documentElement.innerHTML &&
                            _self.response.documentElement.innerHTML.length + 28;
                        break;
                    default:
                        responseSize = _self.response.length;
                }
                _self._eagle_xhr_info.event = event;
                _self._eagle_xhr_info.status = _self.status;
                _self._eagle_xhr_info.success =
                    (_self.status >= 200 && _self.status <= 206) ||
                    _self.status === 304;
                _self._eagle_xhr_info.duration = Date.now() - _self._eagle_start_time; // 统计回来的时间
                _self._eagle_xhr_info.responseSize = responseSize;
                _self._eagle_xhr_info.requestSize = value ? value.length : 0;
                _self._eagle_xhr_info.type = "xhr";
                cb(this._eagle_xhr_info);
            }
        };

        if (this.addEventListener) {
            this.addEventListener("load", ajaxEnd("load"), false);  // 当 load 时执行
            this.addEventListener("error", ajaxEnd("error"), false);
            this.addEventListener("abort", ajaxEnd("abort"), false); // 当 load 时执行
        } else {
            let _origin_onreadystatechange = this.onreadystatechange;
            this.onreadystatechange = function(event) {
                if (_origin_onreadystatechange) {
                    _originOpen.apply(this, arguments);
                }
                if (this.readyState === 4) {
                    ajaxEnd("end")();
                }
            };
        }
        return _originSend.apply(this, arguments);
    };


     // fetch 兼容
    if (window.fetch) {
        let _origin_fetch = window.fetch;
        window.fetch = function() {
            let startTime = Date.now();
            let args = [].slice.call(arguments);  // 将 arguments 转化为数组

            let fetchInput = args[0];
            let method = "GET";
            let url;

            if (typeof fetchInput === "string") {
                url = fetchInput;
            } else if (
                "Request" in window &&
                fetchInput instanceof window.Request
            ) {
                url = fetchInput.url;
                if (fetchInput.method) {
                    method = fetchInput.method;
                }
            } else {
                url = "" + fetchInput;
            }

            if (args[1] && args[1].method) {
                method = args[1].method;
            }

            let fetchData = {
                method: method,
                url: url,
                status: null
            };

            return _origin_fetch.apply(this, args).then(function(response) {
                fetchData.status = response.status;
                fetchData.type = "fetch";
                fetchData.duration = Date.now() - startTime;
                cb(fetchData);
                return response;
            });
        };
    }
  1. 捕获js错误
  • window.onerror
  • window.addEventListener('unhandleRejection') // promise错误
  • 还需要对编译后的源码,用source-map反解析到编译前端源码中,错误位置
  window.onerror = function(message, source, lineno, colno, error){
        console.log("-> error", error);
        let errorInfo = formatError(error)

        errorInfo._message = message
        errorInfo._source = source
        errorInfo._lineno = lineno
        errorInfo._colno = colno

        cb(errorInfo)

        errorInfo.type = 'error'
        _origin_error && _origin_error.apply(window,arguments)
    }
  1. 白屏统计
  • 可以根据具体的业务形态, elementsFromPoint 方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
function getSelector(element) {
	var selector;
	if (element.id) {
		selector = `#${element.id}`;
	} else if (element.className && typeof element.className === "string") {
		selector =
			"." +
			element.className
				.split(" ")
				.filter(function (item) {
					return !!item;
				})
				.join(".");
	} else {
		selector = element.nodeName.toLowerCase();
	}
	return selector;
}
export function blankScreen() {
	const wrapperSelectors = ["body", "html", "#container", ".content"];
	let emptyPoints = 0;
	function isWrapper(element) {
		let selector = getSelector(element);
		if (wrapperSelectors.indexOf(selector) >= 0) {
			emptyPoints++;
		}
	}
	onload(function () {
		let xElements, yElements;
		debugger;
		for (let i = 1; i <= 9; i++) {
			xElements = document.elementsFromPoint(
				(window.innerWidth * i) / 10,
				window.innerHeight / 2
			);
			yElements = document.elementsFromPoint(
				window.innerWidth / 2,
				(window.innerHeight * i) / 10
			);
			isWrapper(xElements[0]);
			isWrapper(yElements[0]);
		}
		if (emptyPoints >= 0) {
			let centerElements = document.elementsFromPoint(
				window.innerWidth / 2,
				window.innerHeight / 2
			);
			tracker.send({
				kind: "stability",
				type: "blank",
				emptyPoints: "" + emptyPoints,
				screen: window.screen.width + "x" + window.screen.height,
				viewPoint: window.innerWidth + "x" + window.innerHeight,
				selector: getSelector(centerElements[0]),
			});
		}
	});
}

数据上报和处理

请求时机

  1. 1像素的gif图,src后面带数据
  2. navigation.sendBean 在浏览器关闭的时候也可以发送
  3. ajax请求

请求兜底

  1. 如果没有 node 中间层,前端缓存一个数组,设置阈值,定时或超量时发送
  2. 如果有,可以先发送到node中去,放在缓存中,再过滤处理下
    1. 不合理的值
    2. 外部大规模的流量攻击

数据处理

  1. 登录身份等,ip信息等
  2. 接入是以sdk形式,每个业务部门有自己唯一的编号
1. type 日志类型,有 performance 性能统计,abnormal 异常统计,或者其他扩展
2. module 业务方部门
3. group 分组,落地页分组
4. dim 维度 , 简单说就是将每个数据都分为多个模块来统计
5. info 性能或异常的具体信息

数据可视化

  1. 落库可以用 ELK 一体的系统
  2. 视图层面,可以从es中查询取,自己再针对个性化用户展示

监控告警

  1. 根据用户设定的一系列阈值,提供电话,邮件,短信等告知情况