乐趣区

关于前端:搭建前端监控如何采集异常数据

大家好,我是杨胜利。

前两篇,咱们介绍了为什么前端应该有监控零碎,以及搭建前端监控的总体步骤,前端监控的 Why 和 What 想必你曾经明确了。接下来咱们解决 How 如何实现的问题。

如果不理解前端监控,倡议先看前两篇:

  • 为什么前端不能没有监控零碎?
  • 前端监控的总体搭建步骤

本篇咱们介绍,前端如何采集数据,先从收集异样数据开始。

什么是异样数据?

异样数据,是指前端在操作页面的过程中,触发的执行异样或加载异样,此时浏览器会抛出来报错信息。

比如说你的前端代码用了个未声明的变量,此时控制台会打印出红色谬误,通知你报错起因。或者是接口申请出错了,在网络面板内也能查到异常情况,是申请发送的异样,还是接口响应的异样。

在咱们理论的开发场景中,前端捕捉的异样次要是分两个大类,接口异样 前端异样,咱们别离看下这两大类异样怎么捕捉。

接口异样

接口异样肯定是在申请的时候触发。前端目前大部分的申请是用 axios 发动的,所以只有获取 axios 可能产生的异样即可。

如果你用 Promise 的写法,则用 .catch 捕捉:

axios
  .post('/test')
  .then((res) => {console.log(res);
  })
  .catch((err) => {
    // err 就是捕捉到的谬误对象
    handleError(err);
  });

如果你用 async/await 的写法,则用 try..catch.. 捕捉:

async () => {
  try {let res = await axios.post('/test');
    console.log(res);
  } catch (err) {
    // err 就是捕捉到的谬误对象
    handleError(err);
  }
};

当捕捉到异样之后,对立交给 handleError 函数解决,这个函数会将接管到的异样进行解决,并调用 上报接口 将异样数据传到服务器,从而实现采集。

下面咱们写的异样捕捉,逻辑上是没问题的,实操起来就会发现第一道坎:页面这么多,难道每个申请都要包一层 catch 吗?

是啊,如果咱们是新开发一个我的项目,在开始的时候就规定每个申请要包一层 catch 也无可非议,然而如果是在一个已有的规模还不小的我的项目中接入前端监控,这时候在每个页面或每个申请 catch 显然是不事实的。

所以,为了最大水平的升高接入老本,缩小侵入性,咱们是用第二种计划:在 axios 拦截器中捕捉异样

前端我的项目,为了对立解决申请,比方 401 的跳转,或者全局谬误提醒,都会在全局写一个 axios 实例,为这个实例增加拦截器,而后在其余页面中间接倒入这个实例应用,比方:

// 全局申请:src/request/axios.js

const instance = axios.create({
  baseURL: 'https://api.test.com'
  timeout: 15000,
  headers: {'Content-Type': 'application/json',},
})

export default instance

而后在具体的页面中这样发动申请:

// a 页面:src/page/a.jsx
import http from '@/src/request/axios.js';

async () => {let res = await http.post('/test');
  console.log(res);
};

这样的话,咱们发现每个页面的申请都会走全局 axios 实例,所以咱们只须要在全局申请的地位捕捉异样即可,就不须要在每个页面捕捉了,这样接入老本会大大降低。

依照这个计划,结下来咱们在 src/request/axios.js 这个文件中入手施行。

拦截器中捕捉异样

首先咱们为 axios 增加响应拦截器:

// 响应拦截器
instance.interceptors.response.use((response) => {return response.data;},
  (error) => {
    // 产生异样会走到这里
    if (error.response) {
      let response = error.response;
      if (response.status >= 400) {handleError(response);
      }
    } else {handleError(null);
    }
    return Promise.reject(error);
  },
);

响应拦截器的第二个参数是在产生谬误时执行的函数,参数就是异样。咱们首先要判断是否存在 error.response,存在就阐明接口有响应,也就是接口通了,然而返回谬误;不存在则阐明接口没通,申请始终挂起,少数是接口解体了。

如果有响应,首先获取状态码,依据状态码来判断什么时候须要收集异样。下面的判断形式简略粗犷,只有状态码大于 400 就视为一个异样,拿到响应数据,并执行上报逻辑。

如果没有响应,能够看作是接口超时异样,调用异样处理函数时传一个 null 即可。

前端异样

下面咱们介绍了在 axios 拦截器中如何捕捉接口异样,这部分咱们再介绍如何捕捉前端异样。

前端代码捕捉异样,最罕用的形式就是用 try..catch.. 了,任意同步代码块都能够放到 try 块中,只有产生异样就会执行 catch:

try {// 任意同步代码} catch (err) {console.log(err);
}

下面说“任意同步代码”而不是“任意代码”,次要是一般的 Promise 写法 try..catch.. 是捕捉不到的,只能用 .catch() 捕捉,如:

try {Promise.reject(new Error('出错了')).catch((err) => console.log('1:', err));
} catch (err) {console.log('2:', err);
}

把这段代码丢进浏览器,打印后果是:

1:Error: 出错了

很显著只是 .catch 捕捉到了异样。不过与下面接口异样的逻辑一样,这种形式解决以后页面异样没什么问题,但从整个利用来看,这样捕捉异样侵入性强,接入老本高,所以咱们的思路仍然是全局捕捉。

全局捕捉 js 的异样也比较简单,用 window.addEventLinstener('error') 即可:

// js 谬误捕捉
window.addEventListener('error', (error) => {// error 就是 js 的异样});

为啥不必 window.onerror?

这里很多小伙伴有疑难,为什么不必 window.onerror 全局监听呢?window.addEventLinstener('error')window.onerror 有什么区别呢?

首先这两个函数性能基本一致,都能够全局捕捉 js 异样。然而有一类异样叫做 资源加载异样,就是在代码中援用了不存在的图片,js,css 等动态资源导致的异样,比方:

const loadCss = ()=> {let link = document.createElement('link')
  link.type = 'text/css'
  link.rel = 'stylesheet'
  link.href = 'https://baidu.com/15.css'
  document.getElementsByTagName('head')[10].append(link)
}
render() {
  return <div>
    <img src='./bbb.png'/>
    <button onClick={loadCss}> 加载款式 <button/>
  </div>
}

上述代码中的 baidu.com/15.cssbbb.png 是不存在的,JS 执行到这里必定会报一个资源找不到的谬误。然而默认状况下,下面两种 window 对象上的全局监听函数都监听不到这类异样。

因为资源加载的异样只会在以后元素触发,异样不会冒泡到 window,因而监听 window 上的异样是捕获不到的。那怎么办呢?

如果你相熟 DOM 事件你就会明确,既然冒泡阶段监听不到,那么在捕捉阶段肯定能监听到。

办法就是给 window.addEventListene 函数指定第三个参数,很简略就是 true,示意该监听函数会在捕捉阶段执行,这样就能监听到资源加载异样了。

// 捕捉阶段全局监听
window.addEventListene(
  'error',
  (error) => {if (error.target != window) {console.log(error.target.tagName, error.target.src);
    }
    handleError(error);
  },
  true,
);

上述形式能够很轻松的监听到图片加载异样,这就是为什么更举荐 window.addEventListene 的起因。不过要记得,第三个参数设为 true,监听事件捕捉,就能够全局捕捉到 JS 异样和资源加载异样。

须要特地留神,window.addEventListene 同样不能捕捉 Promise 异样。不论是 Promise.then() 写法还是 async/await 写法,产生异样时都不能捕捉。

因而,咱们还须要全局监听一个 unhandledrejection 函数来捕捉未解决的 Promise 异样。

// promise 谬误捕捉
window.addEventListener('unhandledrejection', (error) => {
  // 打印异样起因
  console.log(error.reason);
  handleError(error);
  // 阻止控制台打印
  error.preventDefault();});

unhandledrejection 事件会在 Promise 产生异样并且没有指定 catch 的时候触发,相当于一个全局的 Promise 异样兜底计划。这个函数会捕捉到运行时意外产生的 Promise 异样,这对咱们排错十分有用。

默认状况下,Promise 产生异样且未被 catch 时,会在控制台打印异样。如果咱们想阻止异样打印,能够用下面的 error.preventDefault() 办法。

异样处理函数

后面咱们在捕捉到异样时调用了一个异样处理函数 handleError,所有的异样和上报逻辑对立在这个函数内解决,接下来咱们实现这个函数。

const handleError = (error: any, type: 1 | 2) {if(type == 1) {// 解决接口异样}
  if(type == 2) {// 解决前端异样}
}

为了辨别异样类型,函数新加了第二个参数 type 示意以后异样属于前端还是接口。在不同的场景中应用如下:

  • 解决前端异样:handleError(error, 1)
  • 解决接口异样:handleError(error, 2)

解决接口异样

解决接口异样,咱们须要将拿到的 error 参数解析,而后取到须要的数据。接口异样个别须要的数据字段如下:

  • code:http 状态码
  • url:接口申请地址
  • method:接口申请办法
  • params:接口申请参数
  • error:接口报错信息

这些字段都能够在 error 参数中获取,办法如下:

const handleError = (error: any, type: 1 | 2) {if(type == 1) {
    // 此时的 error 响应,它的 config 字段中蕴含申请信息
    let {url, method, params, data} = error.config
    let err_data = {
       url, method,
       params: {query: params, body: data},
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

config 对象中的 params 示意 GET 申请的 query 参数,data 示意 POST 申请的 body 参数,所以我在解决参数的时候,将这两个参数合并为一个,用一个属性 params 来示意。

params: {query: params, body: data}

还有一个 error 属性示意错误信息,这个获取形式要依据你的接口返回格局来拿。要防止获取到接口可能返回的超长错误信息,多半是接口没解决,这样可能会导致写入数据失败,要提前与后盾规定好。

解决前端异样

前端异样异样大多数就是 js 异样,异样对应到 js 的 Error 对象,在解决之前,咱们先看 Error 有哪几种类型:

  • ReferenceError:援用谬误
  • RangeError:超出无效范畴
  • TypeError:类型谬误
  • URIError:URI 解析谬误

这几类异样的援用对象都是 Error,因而能够这样获取:

const handleError = (error: any, type: 1 | 2) {if(type == 2) {
    let err_data = null
    // 监测 error 是否是规范类型
    if(error instanceof Error) {let { name, message} = error
      err_data = {
        type: name,
        error: message
      }
    } else {
      err_data = {
        type: 'other',
        error: JSON.strigify(error)
      }
    }
  }
}

上述判断中,首先判断异样是否是 Error 的实例。事实上绝大部分的代码异样都是规范的 JS Error,但咱们这里还是判断一下,如果是的话间接获取异样类型和异样信息,不是的话将异样类型设置为 other 即可。

咱们轻易写一个异样代码,看一下捕捉的后果:

function test() {console.aaa('ccc');
}
test();

而后捕捉到的异样是这样的:

const handleError = (error: any) => {if (error instanceof Error) {let { name, message} = error;
    console.log(name, message);
    // 打印后果:TypeError console.aaa is not a function
  }
};

获取环境数据

获取环境数据的意思是,不论是接口异样还是前端异样,除了异样自身的数据之外,咱们还须要一些其余信息来帮忙咱们更快更准的定位到哪里出错了。

这类数据咱们称之为“环境数据”,就是触发异样时所在的环境。比方是谁在哪个页面的哪个中央触发的谬误,有了这些,咱们就能马上找到谬误起源,再依据异样信息解决谬误。

环境数据至多包含上面这些:

  • app:利用的名称 / 标识
  • env:应用环境,个别是开发,测试,生产
  • version:利用的版本号
  • user_id:触发异样的用户 ID
  • user_name:触发异样的用户名
  • page_route:异样的页面路由
  • page_title:异样的页面名称

appversion 都是利用配置,能够判断异样呈现在哪个利用的哪个版本。这两个字段我倡议间接获取 package.json 下的 nameversion 属性,在利用降级的时候,及时批改 version 版本号即可。

其余的字段,须要依据框架的配置获取,上面我别离介绍在 Vue 和 React 中如何获取。

在 Vue 中

在 Vue 中获取用户信息个别都是间接从 Vuex 外面拿,如果你的用户信息没有存到 Vuex 里,从 localStorage 里获取也是一样的。

如果在 Vuex 里,能够这样实现:

import store from '@/store'; // vuex 导出目录
let user_info = store.state;
let user_id = user_info.id;
let user_name = user_info.name;

用户信息存在状态治理中,页面路由信息个别是在 vue-router 中定义。前端的路由地址能够间接从 vue-router 中获取,页面名称能够配置在 meta 中,如:

{
  path: '/test',
  name: 'test',
  meta: {title: '测试页面'},
  component: () => import('@/views/test/Index.vue')
},

这样配置之后,获取以后页面路由和页面名称就简略了:

window.vm = new Vue({...})

let route = vm.$route
let page_route = route.path
let page_title = route.meta.title

最初一步,咱们再获取以后环境。以后环境用一个环境变量 VUE_APP_ENV 示意,有三个值:

  • dev:开发环境
  • test:测试环境
  • pro:生产环境

而后在根目录下新建三个环境文件,写入环境变量:

  • .env.development:VUE_APP_ENV=dev
  • .env.staging:VUE_APP_ENV=test
  • .env.production:VUE_APP_ENV=pro

当初获取 env 环境时就能够这么获取:

{env: process.env.VUE_APP_ENV;}

最初一步,执行打包时,传入模式以匹配对应的环境文件:

# 测试环境打包
$ num run build --mode staging
# 生产环境打包
$ num run build --mode production

获取到环境数据,再拼上异样数据,咱们就筹备好了数据期待上报了。

在 React 中

和 Vue 一样,用户信息能够间接从状态治理里拿。因为 React 中没有全局获取以后游览的快捷方式,所以页面信息我也会放在状态治理外面。我用的状态治理是 Mobx,获取形式如下:

import {TestStore} from '@/stores'; // mobx 导出目录
let {user_info, cur_path, cur_page_title} = TestStore;
// 用户信息:user_info
// 页面信息:cur_path,cur_page_title

这样的话,就须要在每次切换页面时,更新 mobx 里的路由信息,怎么做呢?

其实在根路由页(个别是首页)的 useEffect 中监听即可:

import {useLocation} from 'react-router';
import {observer, useLocalObservable} from 'mobx-react';
import {TestStore} from '@/stores';

export default observer(() => {const { pathname, search} = useLocation();
  const test_inst = useLocalObservable(() => TestStore);
  useEffect(() => {test_inst.setCurPath(pathname, search);
  }, [pathname]);
});

获取到用户信息和页面信息,接下来就是以后环境了。和 Vue 一样通过 --mode 来指定模式,并加载相应的环境变量,只不过设置办法略有不同。大多数的 React 我的项目可能都是用 create-react-app 创立的,咱们以此为例介绍怎么批改。

首先,关上 scripts/start.js 文件,这是执行 npm run start 时执行的文件,咱们在结尾局部第 6 行加代码:

process.env.REACT_APP_ENV = 'dev';

没错,咱们指定的环境变量就是 REACT_APP_ENV,因为只有 REACT_ 结尾的环境变量可被读取。

而后再批改 scripts/build.js 文件的第 48 行,批改后如下:

if (argv.length >= 2 && argv[0] == '--mode') {switch (argv[1]) {
    case 'staging':
      process.env.REACT_APP_ENV = 'test';
      break;
    case 'production':
      process.env.REACT_APP_ENV = 'pro';
      break;
    default:
  }
}

此时获取 env 环境时就能够这么获取:

{env: process.env.REACT_APP_ENV;}

总结

通过后面一系列操作,咱们曾经比拟全面的获取到了异样数据,以及产生异样时到环境数据,接下来就是调用上报接口,将这些数据传给后盾存起来,咱们当前查找和追踪就很不便了。

如果你也须要前端监控,无妨花上半个小时,依照文中介绍的办法收集一下异样数据,置信对你很有帮忙。

退出移动版