如何监控前端异常?

一个完善的异常监控平台,是一个非常复杂的项目。它需要包括异常监控、信息收集、信息归类、信息的统计分析、异常场景的重现、异常在源码中的定位等等,同时,也要考虑异常日志的存放以及服务器压力等问题。

本文只涉及异常的捕获、上传方面的内容,大致围绕下面几点展开讨论:

  1. 前端需要处理的异常

  2. 前端异常的捕获方式

  3. 异常信息的上报方式

  4. 异常监控常见问题

为什么要处理异常?

  • 增强用户体验

  • 远程定位问题

  • 未雨绸缪,及早发现问题

  • 无法复线问题,尤其是移动端,机型,系统都是问题

  • 完善的前端方案,前端监控系统;

对于 JS 而言,异常的出现不会直接导致 JS 引擎崩溃,最多只会使当前执行的任务终止。

前端需要处理的异常

  • 语法错误

  • 运行时异常

    • EvalError eval错误

    • RangeError 范围错误

    • ReferenceError 引用错误

    • TypeError 类型错误

    • URIError URI错误

    • SyntaxError 语法错误

    • Error 通用错误

  • 资源加载异常

    • img

    • script

    • link

    • audio

    • video

    • iframe

    • @font-face

    • 外链资源的DOM元素。。。

  • Promise 异常

  • 异步请求异常

    • XMLHttpRequest

    • fetch

前端异常的捕获方式

  • try-catch-finally

  • window.onerror = function () {}

    • img

    • script

    • link

  • window.addEventListener('error', function () {}, true)

  • window.addEventListener("unhandledrejection", function () {})

  • Promise.then().catch(function () {})

  • 封装 XMLHttpRequest、fetch,覆写请求接口对象

try-catch-finally

try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误(如:中文分号)和异步错误(如:回调、promise、setTimeout )就显得无能为力。

try {
    // 模拟一段可能有错误的代码
    throw new Error("会有错误的代码块")
} catch(e){
    // 捕获到try中代码块的错误得到一个错误对象e,进行处理分析
    report(e)
} finally {
    console.log("finally")
}

window.onerror

window.onerror 是一个全局变量,默认值为null。当 JS 运行时错误(包括语法错误)发生时,window 会触发一个 ErrorEvent 接口的事件,并执行 window.onerror();onerror 可以接受多个参数。

window.onerror 无法捕获 静态资源异常、接口异常。

**注意:**语法错误会导致出现语法错误的那个脚本块执行失败,所以语法错误会导致当前代码块运行终止,从而导致整个程序运行中断,如果语法错误这个发生在我们的错误监控语句块中,那么我们就什么也监控不到了。

window.onerror = function (msg, url, row, col, error) {
    // msg:错误信息(字符串)。
    // url:发生错误的脚本URL(字符串)
    // row:发生错误的行号(数字)
    // col:发生错误的列号(数字)
    // error:Error对象(对象)
    console.log('我知道异步错误了');
    console.log({
        msg,  url,  row, col, error
    })
    return true;
};

window.onerror = function () {} 要比其他脚本先执行(注意这个前提!),才可以捕捉到语法错误。

window.onerror函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx

window.addEventListener

监听 js 运行时错误事件,会比 window.onerror 先触发,与 onerror 的功能大体类似,不过事件回调函数传参只有一个保存所有错误信息的参数,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误

当资源(如img或script)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror() 处理函数。这些error事件不会向上冒泡到window,但可以在捕获阶段被捕获。因此如果要全局监听资源加载错误,需要在捕获阶段捕获事件

网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

需要注意:

  • 不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。

  • 需要注意避免 addEventListener 重复监听。

window.addEventListener('error', (msg, url, row, col, error) => {
    console.log('我知道 404 错误了')
    console.log(
        msg, url, row, col, error
    );
    return true
}, true)

unhandledrejection

没有写 catchPromise 中抛出的错误无法被 onerrortry-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。

为了防止有漏掉的 Promise 异常,在全局增加一个对 unhandledrejection 的监听,用来全局监听Uncaught Promise Error

window.addEventListener("unhandledrejection", function(e){
    // Event新增属性
    // @prop {Promise} promise - 状态为rejected的Promise实例
    // @prop {String|Object} reason - 异常信息或rejected的内容
    // 会阻止异常继续抛出,不让Uncaught(in promise) Error产生
    e.preventDefault()
})

Promise.then().catch(function () {})

new Promise(function(resolve, reject) {
    throw 'Uncaught Exception!';
}).catch(function(e) {
    console.log(e); // Uncaught Exception!
})

封装XMLHttpRequest、fetch

/**
 * 函数:封装XMLHttpRequest和fetch对象,获取、上传异常信息。
 */
function captureRequestError (reportLog) {
    // 覆写XMLHttpRequest API
    if(window.XMLHttpRequest) {
        var xmlhttp = window.XMLHttpRequest
        var _oldSend = xmlhttp.prototype.send
        var _handleEvent = function (event) {
            if (event && event.toString() === "[object ProgressEvent]" && event.currentTarget && event.currentTarget.status !== 200) {
                //处理错误信息
            }
        }
        xmlhttp.prototype.send = function () {
            if (this['addEventListener']) {
                this['addEventListener']('error', _handleEvent)
                this['addEventListener']('load', _handleEvent)
                this['addEventListener']('abort', _handleEvent)
                this['addEventListener']('close', _handleEvent)
            } else {
                var _oldStateChange = this['onreadystatechange']
                this['onreadystatechange'] = function (event) {
                    if (this.readyState === 4) {
                        _handleEvent(event)
                    }
                    _oldStateChange && _oldStateChange.apply(this, arguments)
                }
            }
            return _oldSend.apply(this, arguments)
        }
    }

    //覆写fetch API
    if (window.fetch) {
        var _oldFetch = window.fetch
        window.fetch = function() {
            return _oldFetch.apply(this, arguments).then(function(res){
                // 处理信息
                return res
            }).catch(function(error){
                // 处理信息
            })
        }
    }
}

VUE errorHandler

vue本身有监听异常机制,我们可以在它提供的监听函数中,上传异常信息。

Vue.config.errorHandler = (err, vm, info) => {
  console.error('通过vue errorHandler捕获的错误');
  console.error(err);
  console.error(vm);
  console.error(info);
}

崩溃和卡顿

卡顿也就是网页暂时响应比较慢, JS 可能无法及时执行。但崩溃就不一样了,网页都崩溃了,JS 都不运行了,还有什么办法可以监控网页的崩溃,并将网页崩溃上报呢?

利用 window 对象的 loadbeforeunload 事件实现了网页崩溃的监控。

window.addEventListener('load', function () {
    sessionStorage.setItem('good_exit', 'pending');
    setInterval(function () {
        sessionStorage.setItem('time_before_crash', new Date().toString());
    }, 1000);
});

window.addEventListener('beforeunload', function () {
    sessionStorage.setItem('good_exit', 'true');
});

if(sessionStorage.getItem('good_exit') &&
   sessionStorage.getItem('good_exit') !== 'true') {
    /*
        insert crash logging code here
    */
    alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}

以使用 Service Worker 来实现网页崩溃的监控

  • Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;

  • Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;

  • 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。

异常日记上报方式

  1. 异步请求上报, 后端提供接口,或者直接发到日志服务器

  2. img请求上报,url参数带上错误信息

function report(error) {
    var reportUrl = 'http://xxxx/report';
    new Image().src = reportUrl + 'error=' + error;
}

sentry 中文翻译是哨兵。它是一个款错误跟踪、性能监控工具。

Sentry 是一个开源的实时错误报告工具,支持 web 前后端、移动应用以及游戏,支持多种语言(JavaScript、Java、Go、Nodejs、Php、Python 等)和框架(React、Vue、Angular、Next.js 等),还提供了 GitHub、Slack、Trello 等常见开发工具的集成。

使用 sentry 需要结合两个部分:客户端与服务端。客户端就是你需要去监听的项目。而服务端就是一个数据管理平台,它会展示已收集到的错误信息和项目信息,并支持项目管理,组员管理、邮件报警等功能。

可以直接使用 sentry 官方平台,也可以利用 Sentry 的开源库在自己的服务器上搭建服务,官方已经提供了完善的操作文档。 Sentry 的搭建方式主要有两种:Python 安装、通过 Docker 安装。由于 Docker 更加方便易控,官方推荐 Docker 部署。

这里,我们以官方平台为例,简单讲一下接入步骤:

  • 注册账号: 登录入 sentry 官网,注册一个账号。(注意翻墙,shadowsocks PAC 自动模式可能无效,请添加 【PAC 用户自定义规则】或者启用全局模式)。

  • 创建项目,获取DSN: 注册后,在官网创建一个项目,后台会自动生成一个 DSN。DSN 是一个重要的值,用来告诉客户端将事件发送到哪里。

  • 客户端监听项目: 通过 CDN 或者 npm 引入 sentry。

    以 npm 引入为例:

    import Vue from 'vue'
    import * as Sentry from "@sentry/vue"
    Sentry.init({
      Vue,
      dsn: "https://xxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxx",
      integrations: [],
      // Set tracesSampleRate to 1.0 to capture 100%
      // of transactions for performance monitoring.
      // We recommend adjusting this value in production
      tracesSampleRate: 1.0,
      // defaultIntegrations: true
    })
  • 客户端集成:这是客户端的核心部分,它告诉 sentry 怎么收集、收集哪些错误。

    Sentry 默认情况下启用系统集成以集成到标准库或解释器本身。

    defaultIntegrations 用来表示是否使用默认添加的集成。

    integrations 用来标识启用集成的名称列表。列表应该包含所有启用的集成,包括默认的集成。包含默认集成是因为不同的 SDK 版本可能包含不同的默认集成。

    integrations 可用来删除、或添加集成:

    import { ReportingObserver } from "@sentry/integrations"
    Sentry.init({
      dsn: "https://xxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxx",
      integrations: [new ReportingObserver()]
    })

    除引用 sentry 提供的集成,我们可以自定义一个集成,还可以主动捕获并上报错误。

    主动上报的方式有两种: 一种是直接上报文本信息,参数为一个字符串;另一种是上报错误对象,参数为一个 error 对象或者类对象。

    Sentry.captureMessage('error-message', 'fatal')
    
    try {
      console.log(a)
    } catch (error) {
      Sentry.captureException(error)
    }

以下只是简单的使用示例,详细使用可查看下面链接:

https://docs.sentry.io/platforms/javascript/

Sentry For Vue 完整接入详解(2021 Sentry v21.8.x)前方高能预警!三万字,慎入!

常见问题

跨域脚本异常报错信息

生产环境的 JS 做静态资源 CDN 化,导致访问的页面跟脚本文件来自不同的域名,这时候如果没有进行额外的配置,就会容易产生 Script error

Script error 是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error 这样的信息。

在H5的规定中,只要满足下面两个条件,是允许获取跨源脚本的错误信息的。

  1. 客户端在 script 标签上增加 crossorigin 属性;

  2. 服务端设置 js 资源响应头 Access-Control-Origin: 指定域名 | *

window.error 和 window.addEventListener区别

window.onerror 含有详细的 error 信息(如:error.stack),而且兼容性更好,所以一般 JS 运行时错误使用 window.onerror 捕获处理。

window.addEventListener('error') ,可以捕获 JS 运行时的错误,也能捕获资源加载错误。为避免重复上报 js 运行时错误,此时应该只有event.srcElement inatanceof HTMLScriptElement、HTMLLinkElement、HTMLImageElement 时才上报

总结

使用场景分析

  • 可疑区域增加 Try-Catch

  • 全局监控 JS 异常 window.onerror

  • 全局监控静态资源异常 window.addEventListener

  • 捕获没有 CatchPromise 异常:unhandledrejection

  • vue框架: VUE errorHandler 、React 框架: React componentDidCatch

  • 监控网页崩溃:window 对象的 loadbeforeunload

业界已经有的监控平台

  • Sentry 开源 (推荐)

  • 阿里的 ARMS

  • FrontJS

另外还有一些轻量级的 BetterJS

参考资料

前端异常监控-看这篇就够了

如何优雅处理前端异常?

前端代码异常监控实战

如何做前端异常监控?

最后更新于