立即登录

当前页面的脚本发生错误 一篇讲透自研的前端错误监控

11/27 21:21:36

当前页面的脚本发生错误 一篇讲透自研的前端错误监控

// 控制台运行
fetch('/remote/notdefined', {}) 

搜集错误

所有起因来源于错误,那我们如何进行错误捕获。

try/catch

能捕获常规运行时错误,语法错误和异步错误不行

// 常规运行时错误,可以捕获 ✅
try {
  console.log(notdefined);
} catch(e) {
  console.log('捕获到异常:', e);
}

// 语法错误,不能捕获 ❌
try {
  const notdefined,
} catch(e) {
  console.log('捕获到异常:', e);
}

// 异步错误,不能捕获 ❌
try {
  setTimeout(() => {
    console.log(notdefined);
  }, 0)
} catch(e) {
  console.log('捕获到异常:',e);

try/catch有它细致处理的优势,但缺点也比较明显。

window.onerror

pure js错误收集,window.onerror,当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。

/**
* @param {String} message    错误信息
* @param {String} source    出错文件
* @param {Number} lineno    行号
* @param {Number} colno    列号
* @param {Object} error  Error对象
*/

window.onerror = function(message, source, lineno, colno, error) {
   console.log('捕获到异常:', {message, source, lineno, colno, error});

先验证下几个错误是否可以捕获。

// 常规运行时错误,可以捕获 ✅

window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined);

// 语法错误,不能捕获 ❌
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,
      
// 异步错误,可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
  console.log(notdefined);
}, 0)

// 资源错误,不能捕获 ❌
<script>
  window.onerror = function(message, source, lineno, colno, error) {
  console.log('捕获到异常:',{message, source, lineno, colno, error});
  return true;
}
</script>
"https://yun.tuia.cn/image/kkk.png"> 

window.onerror 不能捕获资源错误怎么办?

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。

// 图片、script、css加载错误,都能被捕获 ✅
<script> window.addEventListener('error', (error) => {
   console.log('捕获到异常:', error);
 }, true) </script>
"https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
  
// new Image错误,不能捕获 ❌
<script> window.addEventListener('error', (error) => {
    console.log('捕获到异常:', error);
  }, true) </script>
<script> new Image().src = 'https://yun.tuia.cn/image/lll.png' </script>

// fetch错误,不能捕获 ❌
<script> window.addEventListener('error', (error) => {
    console.log('捕获到异常:', error);
  }, true) </script>
<script> fetch('https://tuia.cn/test') </script> 

new Image运用的比较少,可以单独自己处理自己的错误。

但通用的fetch怎么办呢,fetch返回Promise,但Promise的错误不能被捕获,怎么办呢?

Promise错误

普通Promise错误

try/catch不能捕获Promise中的错误

// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中
try {
  new Promise((resolve,reject) => { 
    JSON.parse('')
    resolve();
  })
} catch(err) {
  console.error('in try catch', err)
}

// 需要使用catch方法
new Promise((resolve,reject) => { 
  JSON.parse('')
  resolve();
}).catch(err => {
  console.log('in catch fn', err)
}) 

async错误

try/catch不能捕获async包裹的错误

const getJSON = async () => {
  throw new Error('inner error')
}

// 通过try/catch处理
const makeRequest = async () => {
    try {
        // 捕获不到
        JSON.parse(getJSON());
    } catch (err) {
        console.log('outer', err);
    }
};

try {
    // try/catch不到
    makeRequest()
} catch(err) {
    console.error('in try catch', err)
}

try {
    // 需要await,才能捕获到
    await makeRequest()
} catch(err) {
    console.error('in try catch', err)

import chunk错误

当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办

import其实返回的也是一个promise,因此使用如下两种方式捕获错误

// Promise catch方法
import(/* webpackChunkName: "incentive" */'./index').then(module => {
    module.default()
}).catch((err) => {
    console.error('in catch fn', err)
})

// await 方法,try catch
try {
    const module = await import(/* webpackChunkName: "incentive" */'./index');
    module.default()
} catch(err) {
    console.error('in try catch', err)

小结:全局捕获Promise中的错误

以上三种其实归结为Promise类型错误,可以通过unhandledrejection捕获

// 全局统一处理Promise
window.addEventListener("unhandledrejection"function(e){
  console.log('捕获到异常:', e);
});
fetch('https://tuia.cn/test'

为了防止有漏掉的 Promise 异常,可通过unhandledrejection用来全局监听Uncaught Promise Error。

Vue错误

由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler。

/**
 * 全局捕获Vue错误,直接扔出给onerror处理
 */
Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })

React错误

react 通过componentDidCatch,声明一个错误边界的组件

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return 

Something went wrong.

;
    }

    return this.props.children; 
  }
}

class App extends React.Component {
   
  render() {
    return (
    
      
    
  
    )
  }

但error boundaries并不会捕捉以下错误:React事件处理,异步代码,error boundaries自己抛出的错误。

跨域问题

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。

如果当前投放页面和云端JS所在不同域名,如果云端JS出现错误,window.onerror会出现Script Error。通过以下两种方法能给予解决。

<script src="http://yun.tuia.cn/test.js" crossorigin></script>

const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = 'http://yun.tuia.cn/test.js';
document.body.appendChild(script); 




  Test page <span style="color: rgb(198, 120, 221);line-height: 26px;">in</span> http://test.com

<body>
  <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
  <script> window.onerror = function (message, url, line, column, error) {
    console.log(message, url, line, column, error);
  }

  try {
    foo(); // 调用testerror.js中定义的foo方法
  } catch (e) {
    throw e;
  } </script>

 

会发现如果不加try catch,console.log就会打印script error。加上try catch就能捕获到。

我们捋一下场景,一般调用远端js,有下列三种常见情况。

调用方法场景

可以通过封装一个函数,能装饰原方法,使得其能被try/catch。

 


  Test page <span style="color: rgb(198, 120, 221);line-height: 26px;">in</span> http://test.com

<body>
  <script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
  <script> window.onerror = function (message, url, line, column, error) {
    console.log(message, url, line, column, error);
  }

  function wrapErrors(fn) {
    // don't wrap function more than once
    if (!fn.__wrapped__) {
      fn.__wrapped__ = function () {
        try {
          return fn.apply(this, arguments);
        } catch (e) {
          throw e; // re-throw the error
        }
      };
    }

    return fn.__wrapped__;
  }

  wrapErrors(foo)() </script>

 

大家可以尝试去掉wrapErrors感受下。

事件场景

可以劫持原生方法。

当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办

 


  Test page <span style="color: rgb(198, 120, 221);line-height: 26px;">in</span> http://test.com

<body>
  <script> const originAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function (type, listener, options) {
      const wrappedListener = function (...args) {
        try {
          return listener.apply(this, args);
        }
        catch (err) {
          throw err;
        }
      }
      return originAddEventListener.call(this, type, wrappedListener, options);
    } </script>
  "height: 9999px;">http://test.com

  <script src="https://yun.dui88.com/tuia/cdn/remote/error_scroll.js"></script>
  <script> window.onerror = function (message, url, line, column, error) {
    console.log(message, url, line, column, error);
  } </script>

 

大家可以尝试去掉封装EventTarget.prototype.addEventListener的那段代码,感受下。

上报接口

为什么不能直接用GET/POST/HEAD请求接口进行上报?

这个比较容易想到原因。一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。

为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。而且载入js/css资源还会阻塞页面渲染当前页面的脚本发生错误,影响用户体验。

构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。

使用new Image进行接口上报。最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。

首先,1x1像素是最小的合法图片。而且当前页面的脚本发生错误,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG。

同样的响应卡盟,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。GIF才是最佳选择。

使用1*1的gif[1]

非阻塞加载

尽量避免SDK的js资源加载影响。

通过先把window.onerror的错误记录进行缓存,然后异步进行SDK的加载,再在SDK里面处理错误上报。


"en">

    <script> (function(w) {
            w._error_storage_ = [];
            function errorhandler(){
                // 用于记录当前的错误 
                w._error_storage_&&w._error_storage_.push([].slice.call(arguments));
            } 
            w.addEventListener && w.addEventListener("error", errorhandler, true);
            var times = 3,
            appendScript = function appendScript() {
                var sc = document.createElement("script");
                sc.async = !0,
                sc.src = './build/skyeye.js',  // 取决于你存放的位置
                sc.crossOrigin = "anonymous",
                sc.onerror = function() {
                    times--,
                    times > 0 && setTimeout(appendScript, 1500)
                },
                document.head && document.head.appendChild(sc);
            };
            setTimeout(appendScript, 1500);
        })(window); </script>

<body>
    

这是一个测试页面(new)



 

采集聚合端(日志服务器)

这个环节,输入是接口接收到的错误记录,输出是有效的数据入库。核心功能需要对数据进行清洗,顺带解决了过多的服务压力。另一个核心功能是对数据进行入库。

总体流程可以看为错误标识 -> 错误过滤 -> 错误接收 -> 错误存储。

错误标识(SDK配合)

聚合之前,我们需要有不同维度标识错误的能力,可以理解为定位单个错误条目,单个错误事件的能力。

单个错误条目

通过date和随机值生成一条对应的错误条目id。

const errorKey = `${+new Date()}@${randomString(8)}`

function randomString(len) {  
    len = len || 32;
    let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
    let maxPos = chars.length;
    let pwd = '';  
    for (let i = 0; i < len; i++) {    
        pwd += chars.charAt(Math.floor(Math.random() * maxPos));  
    }  
    return pwd;

单个错误事件

首先需要有定位同个错误事件(不同用户,发生相同错误类型、错误信息)的能力。

gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办_当前页面的脚本发生错误

通过message、colno与lineno进行相加计算阿斯克码值,可以生成错误的errorKey。

const eventKey = compressString(String(e.message), String(e.colno) + String(e.lineno))

function compressString(str, key) {
    let chars = 'ABCDEFGHJKMNPQRSTWXYZ';
    if (!str || !key) {
        return 'null';
    }
    let n = 0,
        m = 0;
    for (let i = 0; i < str.length; i++) {
        n += str[i].charCodeAt();
    }
    for (let j = 0; j < key.length; j++) {
        m += key[j].charCodeAt();
    }
    let num = n + '' + key[key.length - 1].charCodeAt() + m + str[str.length - 1].charCodeAt();
    if(num) {
        num = num + chars[num[num.length - 1]];
    }
    return num;

如下图,一个错误事件(事件列表),下属每条即为实际的错误条目。

当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办

错误过滤(SDK配合)

域名过滤

过滤本页面script error,可能被webview插入其他js。

我们只关心自己的远端JS问题,因此做了根据本公司域名进行过滤。

// 伪代码
if(!e.filename || !e.filename.match(/^(http|https)://yun./)) return true 

重复上报

怎么避免重复的数据上报?根据errorKey来进行缓存,重复的错误避免上报的次数超过阈值。

// 伪代码

const localStorage = window.localStorage;
const TIMES = 6; // 缓存条数

export function setItem(key, repeat) {
    if(!key) {
        key = 'unknow';
    }
  
    if (has(key)) {
        const value = getItem(key);
        
       // 核心代码,超过条数,跳出
        if (value >= repeat) {
            return true;
        }
        storeStorage[key] = {
            value: value + 1,
            time: Date.now()
        }
    } else {
        storeStorage[key] = {
            value: 1,
            time: Date.now()
        }
    }
    return false;

错误接收

在处理接收接口的时候,注意流量的控制,这也是后端开发需要投入最多精力的地方,处理高并发的流量。

错误记录

接收端使用Koa,简单的实现了接收及打印到磁盘。

// 伪代码

module.exports = async ctx => {
  const { query } = ctx.request;
  
  // 对于字段进行简单check 
  check([ 'mobile''network''ip''system''ua', ......], query);

  ctx.type = 'application/json';
  ctx.body = { code: '1', msg: '数据上报成功' };

  // 进行日志记录到磁盘的代码,根据自己的日志库选择
}; 

削峰机制

比如每秒设置2000的阈值,然后根据请求量减少上限,定时重置上限。

// 伪代码

// 1000ms
const TICK = 1000;
// 1秒上限为2000
const MAX_LIMIT = 2000;
// 每台服务器请求上限值
let maxLimit = MAX_LIMIT;

/**
 * 启动重置函数
 */
const task = () => {
  setTimeout(() => {
    maxLimit = MAX_LIMIT;
    task();
  }, TICK);
};
task();

const check = () => {
  if (maxLimit <= 0) {
    throw new Error('超过上报次数');
  }
  maxLimit--;
  // 执行业务代码。。。
}; 

采样处理

超过阈值,还可以进行采样收集。

// 只采集 20%
if(Math.random() < 0.2) {
  collect(data)      // 记录错误信息

错误存储

对于打印在了磁盘的日志,我们怎么样才能对于其进行聚合呢,这里得考虑使用存储方案。

一般选择了存储方案后,设置好配置,存储方案就可以通过磁盘定时周期性的获取数据。因此我们需要选择一款存储方案。

gocom打开oa提示 当前网页的脚本发生错误_当前页面的脚本发生错误_当前页的脚本发生错误怎么办

对于存储方案,我们对比了日常常见方案,阿里云日志服务 - Log Service(SLS)、ELK(Elastic、Logstash、Kibana)、Hadoop/Hive(将数据存储在 Hadoop,利用 Hive 进行查询) 类方案的对比。

从以下方面进行了对比,最终选择了Log Service,主要考虑为无需搭建,成本低,查询功能满足。

功能项ELK 类系统Hadoop + Hive日志服务

日志延时

1~60 秒

几分钟~数小时

实时

查询延时

小于 1 秒

分钟级

小于 1 秒

查询能力

扩展性

提前预备机器

提前预备机器

秒级 10 倍扩容

成本

较高

较低

很低

日志延时:日志产生后,多久可查询。查询延时:单位时间扫描数据量。查询能力:关键词查询、条件组合查询、模糊查询、数值比较、上下文查询。扩展性:快速应对百倍流量上涨。成本:每 GB 费用。

具体API使用,可查看日志服务[2]。

可视分析端(可视化平台)

当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办

这个环节,输入是接口接收到的错误记录,输出是有效的数据入库。核心功能需要对数据进行清洗,顺带解决了过多的服务压力。另一个核心功能是对数据进行入库。

主功能

这部分主要是产品功能的合理设计,做到小而美,具体的怎么聚合,参考阿里云SLS就可以。

首页图表,可选1天、4小时、1小时等等,聚合错误数,根据1天切分24份来聚合。首页列表,聚合选中时间内的数据,展示错误文件、错误key、事件数、错误类型、时间、错误信息。错误详情,事件列表、基本信息、设备信息、设备占比图表(见上面事件列表的图)。

当前页的脚本发生错误怎么办_当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误

image.png排行榜

刚开始做了待处理错误列表、我的错误列表、已解决列表,错误与人没有绑定关系,过于依赖人为主动,需要每个人主动到平台上处理,效果不佳。

后面通过错误作者排行榜,通过钉钉日报来提醒对应人员处理。紧急错误,通过实时告警来责任到人,后面告警会说。

具体原理:

gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办_当前页面的脚本发生错误

image.pngSourceMap

利用webpack的hidden-source-map构建。与 source-map 相比少了末尾的注释,但 output 目录下的 index.js.map 没有少。线上环境避免source-map泄露。

webpackJsonp([1],[
  function(e,t,i){...},
  function(e,t,i){...},
  function(e,t,i){...},
  function(e,t,i){...},
  ...
])
// 这里没有生成source-map的链接地址 

根据报错文件的url,根据团队内部约定好的目录和规则,定位之前打包上传的sourceMap地址。

const sourcemapUrl = ('xxxfolder/' + url + 'xxxHash' +'.map'

获取上报的line、column、source,利用第三方库sourceMap进行定位。

const sourceMap = require('source-map')

// 根据行数获取源文件行数
const getPosition = async(map, rolno, colno) => {
  const consumer = await new sourceMap.SourceMapConsumer(map)

  const position = consumer.originalPositionFor({
    line: rolno,
    column: colno
  })

  position.content = consumer.sourceContentFor(position.source)

  return position

当前页的脚本发生错误怎么办_当前页面的脚本发生错误_gocom打开oa提示 当前网页的脚本发生错误

感兴趣SourceMap原理的,可以继续深入,SourceMap 与前端异常监控[3]。

错误报警报警设置每条业务线设置自己的阈值、错误时间跨度,报警轮询间隔通过钉钉hook报警到对应的群通过日报形式报出错误作者排行榜

当前页的脚本发生错误怎么办_gocom打开oa提示 当前网页的脚本发生错误_当前页面的脚本发生错误

image.png四、扩展行为搜集

通过搜集用户的操作,可以明显发现错误为什么产生。

gocom打开oa提示 当前网页的脚本发生错误_当前页的脚本发生错误怎么办_当前页面的脚本发生错误

分类搜集方式点击行为

使用addEventListener监听全局上的click事件,将事件和DOM元素名字收集。与错误信息一起上报。

发送请求

来源:【九爱网址导航www.fuzhukm.com】 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

最新资讯