前端页面监控-异常

在系统运行过程中,前端页面可能会出现各种原因导致的异常,比如js运行错误,资源加载异常等。

开发人员可以通过浏览器开发工具(控制台)跟踪和定位相关问题,但客户和一般的运维人员是无法帮我们排查此类问题的。

所以对前端页面异常的监控就很有必要,通过将监控到的异常信息发送回服务器端,开发人员可以方便的定位和解决问题,从而增强系统的健壮性。

监控前端页面异常的主要思路是通过在window上添加异常监听器来实现。

常见的前端页面错误有以下三类:

  • js运行时异常
  • 资源加载异常(js/css/img)
  • api调用异常(ajax)

对于api调用异常,一方面应该会有相应提示,另外一方面服务器端也会有相应的日志可查,所以在此不做介绍,本文主要介绍前两种异常的监控和相关处理。

js运行时异常

js运行时异常根据监听事件类型的不同分为以下两类。

  • 一般js运行时异常

    对于一般的运行时异常,可以通过window.addEventListener('error', handler)来捕获处理。

                            
                                /**
                                 * taozh1982@gmail.com
                                 * www.zhangyiheng.com
                                 */
                                window.addEventListener('error', function (event) {
                                    console.log(event.type);    //"error"
                                    console.log(event.lineno);  //发生异常的行号,ex)20
                                    console.log(event.colno);   //发生异常的列号,ex)10
                                    console.log(event.message); //错误消息,ex)Uncaught TypeError: Cannot set property 'error' of undefined
                                    console.log(event.error);   //错误对象,包括消息和错误堆栈信息
                                    console.log(event.target);  //引发异常的对象
                                    event.preventDefault();     //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。
                                    sendToServer(event.error);
                                }, true);                       // true表示在捕获阶段事件
    
                                //测试代码,由于abc对象不存在引发的异常
                                window.abc.error = '123';
                            
                        
  • Promise未捕获异常

    当在Promise中调用reject,并且错误信息没有被处理的时候,会抛出一个unhandledrejection类型的异常事件, 但是这个错误不会被window.addEventListener('error', listener)捕获,需要添加window.addEventListener('unhandledrejection', handler)来捕获处理。

                            
                                /**
                                 * taozh1982@gmail.com
                                 * www.zhangyiheng.com
                                 */
                                window.addEventListener('unhandledrejection', function (event) {
                                    console.log(event.type);    //"unhandledrejection"
                                    console.log(event.reason);  //异常原因
                                    console.log(event.promise); //引发异常的Promise对象
                                    event.preventDefault();     //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。
                                    sendToServer(event.reason);
                                }, true);                       // true表示在捕获阶段事件
    
                                //测试代码,未捕获Promise的reject引发的异常
                                new Promise(function (resolve, reject) {
                                    reject('promise reject');
                                });
                            
                        

资源加载异常

资源加载异常也可以通过window.addEventListener('error', handler)来捕获处理,清注意,因为网络请求异常事件不会冒泡,添加的必须是捕获阶段的事件监听。

                
                    /**
                     * taozh1982@gmail.com
                     * www.zhangyiheng.com
                     */
                    window.addEventListener('error', function (event) {
                        console.log(event.type);    //"error"
                        console.log(event.target);  //引发异常的对象,ex)img
                        event.preventDefault();     //添加preventDefault可以阻止浏览器默认的异常处理(输出到console控制台)。
                    },true);                        //此处必须为true(捕获阶段事件),否则监听不到

                    //测试代码,资源不存在引发的异常
                    <img src="../resource/a.png">
                    <script src="../resource/a.js"> </script>
                    <link href="../resource/a.css" rel="stylesheet" type="text/css">

                
            

一般js运行时异常和资源加载异常,都可以通过window.addEventListener('error', handler)来处理,要区分这两种异常,可以通过event的类型进行判断判断

                
                    /**
                     * taozh1982@gmail.com
                     * www.zhangyiheng.com
                     */
                    window.addEventListener('error', function (event) {
                        if(event instanceof Event){
                            console.log("resource error");
                        }else if(event instanceof ErrorEvent){
                            console.log("js error");
                        }
                    }, true);
                
            

请注意,以上关于js运行时异常和资源加载异常的事件监听要放在页面的最前面,否则可能无法捕获到异常的发生。

异常处理

捕获到的异常,可以通过ajax发送到服务器端,记入log日志,以方便后续跟踪查询。

简单示例如下:

                
                    /**
                     * taozh1982@gmail.com
                     * www.zhangyiheng.com
                     */
                    function sendToServer(errorData) {
                        var xhr = new XMLHttpRequest();
                        xhr.open("post", "/api/page-monitor/");
                        xhr.send(errorData)
                    }
                
            

实现代码

以下我的实现代码,将其引入到页面最前面即可(需要修改服务器地址),实现了异常和性能监控,性能监控部分请参考 前端页面监控-性能

                
                    /**
                     * taozh1982@gmail.com
                     * www.zhangyiheng.com
                     */
                    (function (window) {
                        var $monitor_server_url = "";//服务器地址
                        var $monitor_performance_navigation = {//导航计时阈值
                            duration: 10000
                        };
                        var $monitor_performance_resource = {//资源计时阈值
                            duration: 10000
                        };
                        var $Monitor = {
                            init: function () {
                                $Monitor.initErrMonitor();
                                $Monitor.initPerformanceMonitor();
                            },
                            initErrMonitor: function () {
                                window.addEventListener('error', function (event) {
                                    if (event instanceof ErrorEvent) {
                                        $Monitor.handleJSErr(event);
                                    } else if (event instanceof Event) {
                                        $Monitor.handleResourceErr(event);
                                    }
                                }, true);
                                window.addEventListener('unhandledrejection', $Monitor.handleUnhandledRejection, true);
                            },
                            handleJSErr: function (event) {
                                // console.log("js error");
                                $Monitor.sendErrToServer({
                                    error_type: "javascript",
                                    error: {
                                        lineno: event.lineno,
                                        colno: event.colno,
                                        message: event.message,
                                        stack: event.error.stack
                                    }
                                }, event);
                            },
                            handleResourceErr: function (event) {
                                // console.log("resource error");
                                $Monitor.sendErrToServer({
                                    error_type: "resource",
                                    error: {
                                        target: event.target.outerHTML
                                    }
                                }, event);
                            },
                            handleUnhandledRejection: function (event) {
                                // console.log("unhandledrejection");
                                $Monitor.sendErrToServer({
                                    error_type: "unhandledrejection",
                                    error: {
                                        reason: event.reason,
                                        promise: event.promise
                                    }
                                }, event);
                            },
                            sendErrToServer: function (errData, event) {
                                errData.monitor_type = "error";
                                $Monitor._sendToServer(errData);
                            },

                            initPerformanceMonitor: function () {
                                window.addEventListener('load', function (event) {
                                    setTimeout($Monitor.handlePerformance, 1000);
                                });
                            },
                            handlePerformance: function () {
                                var navigationTiming = $Monitor.getNavigationTiming();
                                var timing = {resource: []};
                                if ($Monitor.checkPerformance(navigationTiming, $monitor_performance_navigation) === false) {
                                    timing.navigation = navigationTiming;
                                }

                                var resourceTiming = $Monitor.getResourceTiming();
                                resourceTiming.forEach(function (resourceTimingItem) {
                                    if ($Monitor.checkPerformance(resourceTimingItem, $monitor_performance_resource) === false) {
                                        timing.resource.push(resourceTimingItem);
                                    }
                                });
                                if (timing.navigation || timing.resource.length > 0) {
                                    $Monitor.sendPerformanceToServer({
                                        timing: timing
                                    });
                                }
                            },
                            sendPerformanceToServer: function (performanceData) {
                                performanceData.monitor_type = "performance";
                                $Monitor._sendToServer(performanceData);
                            },
                            checkPerformance: function (timing, monitor_performance) {
                                if (!timing || !monitor_performance) {
                                    return true;
                                }
                                var keys = Object.keys(timing);
                                var len = keys.length;
                                for (var i = 0; i < len; i++) {
                                    var key = keys[i];
                                    if (monitor_performance.hasOwnProperty(key)) {
                                        if (timing[key] > monitor_performance[key]) {
                                            return false;
                                        }
                                    }
                                }
                                return true
                            },
                            getNavigationTiming: function () {
                                var performance = window.performance;
                                if (!performance) {
                                    return null;
                                }
                                var navigationTiming = performance.getEntriesByType('navigation')[0];
                                if (!navigationTiming) {
                                    navigationTiming = performance;
                                    var navigation = navigationTiming.navigation;
                                    var type = navigation.type;
                                    switch (type) {
                                        case 0:
                                            type = "navigate";
                                            break;
                                        case 1:
                                            type = "reload";
                                            break;
                                        case 2:
                                            type = "back_forward ";
                                            break;
                                    }
                                    navigationTiming = navigationTiming.timing.toJSON();
                                    navigationTiming.startTime = navigationTiming.startTime || navigationTiming.navigationStart;
                                    navigationTiming.name = window.location.href;
                                    navigationTiming.type = type;
                                    navigationTiming.initiatorType = "navigation";
                                    navigationTiming.redirectCount = navigation.redirectCount;
                                    navigationTiming.duration = navigationTiming.loadEventEnd - navigationTiming.startTime;
                                } else {
                                    navigationTiming = navigationTiming.toJSON();
                                }
                                return Object.assign($Monitor._getPerformanceTiming(navigationTiming), {
                                    /*
                                     导航类型
                                     navigate/reload/back_forward/prerender
                                     */
                                    type: navigationTiming.type,
                                    /*
                                    执行 onload 回调函数的时间
                                    -- 减少onload回调函数操作,
                                    */
                                    loadEvent: navigationTiming.loadEventEnd - navigationTiming.loadEventStart,
                                    /*
                                    解析dom树时间
                                    -- 减少dom树嵌套
                                    */
                                    domReady: navigationTiming.domComplete - navigationTiming.domInteractive
                                });
                            },
                            getResourceTiming: function () {
                                var timingArr = [];
                                var performance = window.performance;
                                if (!performance) {
                                    return timingArr;
                                }
                                performance.getEntriesByType('resource').forEach(function (timing) {
                                    timingArr.push($Monitor._getPerformanceTiming(timing.toJSON()))
                                });
                                return timingArr;
                            },
                            _getPerformanceTiming: function (timingObj) {
                                return {
                                    /*
                                     地址
                                     */
                                    name: timingObj.name,
                                    /*
                                     导航/资源的类型,navigation/link/script/css/img/iframe
                                     */
                                    initiatorType: timingObj.initiatorType,
                                    /*
                                    重定向计数
                                    */
                                    redirectCount: timingObj.redirectCount | 0,
                                    /*
                                    重定向时间
                                    -- 直接使用重定向之后的地址,可以减少重定向耗时。
                                    */
                                    redirect: timingObj.redirectEnd - timingObj.redirectStart,
                                    /*
                                    DNS查询时间
                                    -- 减少DNS的请求次数
                                    -- DNS预获取 
                                    */
                                    domainLookup: timingObj.domainLookupEnd - timingObj.domainLookupStart,
                                    /*
                                    TCP连接时间
                                    -- 减少HTTP的请求数量,
                                        1#资源合并
                                        2#长链接
                                    */
                                    connect: timingObj.connectEnd - timingObj.connectStart,
                                    /*
                                    SSL/TLS协商时间
                                    */
                                    secureConnection: timingObj.connectEnd - timingObj.secureConnectionStart,
                                    /*
                                    第一字节的到达时间,浏览器请求页面与从服务器接收到信息的第一字节之间的时间
                                    */
                                    ttfb: timingObj.responseStart - timingObj.requestStart,
                                    /*
                                     内容下载时间
                                     -- 内容压缩,gzip
                                     */
                                    contentDownload: timingObj.responseEnd - timingObj.responseStart,
                                    /*
                                     整个导航完成/资源可用时间
                                     */
                                    duration: timingObj.duration
                                }
                            },
                            _sendToServer: function (monitorData) {
                                if (!$monitor_server_url) {
                                    return;
                                }
                                window.setTimeout(function () {
                                    monitorData.url = window.location.href;
                                    var httpRequest = new XMLHttpRequest();
                                    httpRequest.open("POST", $monitor_server_url);
                                    httpRequest.setRequestHeader("Content-Type", "application/json; charset=utf-8");
                                    httpRequest.send(JSON.stringify(monitorData));
                                }, 100);
                            }
                        };
                        $Monitor.init();
                    })(window);
                    //test code
                    //js error test
                    /* window.abc.error = '123'; */

                    //unhandledrejection test
                    /*
                    new Promise(function (resolve, reject) {
                        reject('promise reject');
                    });
                    */

                    //resource error
                    /* <img src="../resource/notfound.png"> *