前端监控进阶篇 — Sentry 监控 Next.js 项目实践

7,313 阅读11分钟

前言

上一篇相关文章介绍了 Sentry 的基础篇 —— 本地安装,本篇文章以实际项目出发使用 Sentry 进行前端监控实践。实践案例选择 Next.js 项目,具体项目地址next-sentry-easy

上一篇:前端监控基础篇 —— Docker + Sentry 搭建前端监控系统

开源社区存在很多搭建 Sentry 的文章,但是关于相关细节配置使用的实践文章其实并不多,对于新手来说不是很友好,Next.js 相关的就更少之又少了。 Next.js 国内社区并不算庞大,关于 Next.js 的监控说实话我也没找到太完美的方案,所以自己也尝试做了几个方案,Sentry 就是其中的一个选择。写这篇文章的原因还有如下的原因:

Next.js 官方示例仓库里有两个关于 sentry 的 Demo,这两个 Demo 都存在一些问题。

  • 第一个with-sentry-simple是一个最基础的简单示例,但是此 Demo 存在 bug —— 只在客户端可以正常上报,而服务端并没有成功上报。

  • 第二个with-sentry是一个比较复杂的案例,问题呢就是太复杂了,不适合新手上手学习。

所以,这里就综合一下,弄一个比较适合新手又比较完整的 Demo。

新建 Sentry 项目

新创建一个 Node.js 的项目,新建好之后 Sentry 就会为项目分配一个 DSN 地址。如下图所示:

初始化项目

  • package.json
"@sentry/browser": "^5.11.0",
"@sentry/node": "^5.11.0",
  • next.config.js
const withSourceMaps = require('@zeit/next-source-maps')

module.exports = withSourceMaps({
  env: {
    SENTRY_DSN: 'http://e69fa8afd38e43e59fc3baf48ec0c681@localhost:9000/11'
  },
  webpack: (config, options) => {
    if (!options.isServer) {
      config.resolve.alias['@sentry/node'] = '@sentry/browser'
    }
    return config
  },
})

上面这么配置是因为 Next.js 的特殊性,服务端渲染框架,既存在服务端也存在客户端场景,项目里引入的只能是一种,通用引入@sentry/node,所以当客户端的时候,要替换成@sentry/browser

  • _app.js
  import * as Sentry from '@sentry/node'
    
  Sentry.init({
    // Replace with your project's Sentry DSN
    dsn: process.env.SENTRY_DSN,
  });

  static async getInitialProps ({ Component, ctx }) {
    let pageProps = {};
    try {
      if (Component.getInitialProps) {
        pageProps = await Component.getInitialProps({ ctx })
      }
      return { pageProps }
    } catch (err) {
      // This will work on both client and server sides.
      console.log('The Error happened in: ', typeof window === 'undefined' ? 'Server' : 'Client');
      Sentry.captureException(err)
      return { pageProps };
    }
  }

在 _app.js 里对 Sentry 进行初始化配置,这里简单只配置了 dsn 参数。另外,在 getInitialProps 函数里进行了操作,如果发生错误,使用 Sentry.captureException()进行上报。

注意,此处主要负责页面渲染,路由跳转时的错误拦截,可以拦截到客户端和服务端两种场景的错误。具体为什么可以去查看我之前写的 Next.js 相关文章。

  • _error.js
import * as Sentry from '@sentry/node';

const MyError = ({ statusCode, err }) => {
  if (err) {
    // This will work on both client and server sides in production.
    Sentry.captureException(err);
  }

  return <Error statusCode={statusCode} />
}

_error.js 我们都知道,是 Next.js 内置的错误组件,程序在运行时出现错误就会渲染该组件,因此,在这里主要负责程序运行时的错误拦截,比如点击按钮、提交表单等交互操作的错误。

效果

上面初始化工作已经完成,来运行项目简单查看一下效果:

  • 运行时出现错误

如下图所示,分别点击客户端和服务端的错误页面。

  • Sentry 项目 Issue 列表

如下图所示, Sentry 正常捕获到了代码里所抛出的异常。

  • 邮件提醒

如下图所示,错误还可以通过邮件发送给开发者,更为方便及时的提醒。

扩展项目 —— 捕获数据请求

上面完成了初始化项目,可以正常捕获客户端、服务端、初始化以及运行中的异常错误。不过呢,在使用过程中发现 Sentry 并不能捕获网络请求的错误。比如,项目里使用的都是 Fetch,如下图所示,初始化页面的网络请求报错404,但是 Sentry 的控制台并没有捕获到错误。

fetch-error-not-report

正常的商业系统,当用户多且业务逻辑复杂的时候,有一定需求需要对网络请求也进行简单的监控。所以,简单来扩展一下项目,使其能捕获数据请求的异常。

  • 封装 FetchError
class FetchError extends Error {
  constructor(url = 'http://localhost', props) {

    super(props);
    const { message, traceId, userId } = props;

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, FetchError)
    }

    this.name = 'FetchError'
    // Custom debugging information
    this.url = url
    this.message = message
    traceId && (this.traceId = traceId)
    userId && (this.userId = userId)
    this.date = new Date()
    console.log(this)
  }
}

export default FetchError
  • 封装 fetch
import fetch from 'isomorphic-unfetch';
import * as Sentry from '@sentry/node';
import FetchError from './FetchError';

function dealStatus(res) {
  if (res.status !== 200) {
    const err = new FetchError(res.url, {
      traceId: Math.random() * 10000, // create your application traceId
      userId: 26,
      message: `${typeof window !== 'undefined' ? 'Client' : 'Server'} fetch error - ${res.status}`
    })
    // This will work on both client and server sides in production.
    Sentry.captureException(err);
  }
  return res;
}

// initial fetch
const unfetch = Object.create(null);

...

HTTP_METHOD.forEach(method => {
  // is can send data in opt.body
  const bodyData = BODY_METHOD.includes(method);
  unfetch[method] = (path, { data } = {}) => {
    let url = path;
    const opts = {
      method,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      mode: 'cors',
      cache: 'no-cache'
    };

    ...

    return fetch(url, opts)
      .then(dealStatus)
      .then(res => res.json());
  };
});

export default unfetch;

上面,简单的封装了 Fetch 函数,然后在里面处理了错误异常,如果返回码不是 200,表示本次请求出现错误,使用Sentry.captureException(err);上报错误。

具体效果如下图所示:

可以见到,Sentry 成功上报了数据请求的异常信息。

这里其实存在一个问题,如果恰好服务器崩溃了怎么办?岂不是一天开发人员要收到无数封的邮件轰炸?不是很合理嘛,能不能 Fetch Error 不发邮件呢?这一点在下文会提到~

扩展项目 —— 源码定位

源码定位是什么意思呢,我们先来看看现在正常的错误上报信息,如下图所示:

从上图可以看出来,因为代码是经过压缩的,所以错误的上下文,堆栈信息也是压缩过后的,这样的代码排查问题相当困难了,只是有个报错而已。那么众所周知,我们的构建工具一般来说也就是 webpack,可以配置 source-map 来对代码进行映射。在这里,Sentry 也可以,通过配置 release 版本信息就可以更精确的定位错误信息。

Sentry官方给出来的配置方案有三种,release API、sentry-cli和sentry-webpack-plugin,前两个都需要手动上传项目的 source-map 文件,第三个通过代码配置在项目 build 的时候进行上传,所以这里采用第三种,更为方便。其他的社区都有对应文章,可以去看。

  • 第一步:安装依赖
yarn add @zeit/next-source-maps @sentry/webpack-plugin
  • 第二步:配置 .sentryclirc
[defaults]
url = http://localhost:9000/
org = luffyzh
project = next-sentry-example

[auth]
token = your token api

这个配置文件是配置 release 的关键文件,默认参数是上报路径,组织名称以及项目名称,通过这三个信息就可以准确定位到具体的项目了。然后 auth 下面的 token 需要我们去生成,具体如下图:

生成 token 的时候一定要勾选 project:write 权限才可以!!!

  • 第三步:配置 next.config.js
const webpack = require('webpack')
const withSourceMaps = require('@zeit/next-source-maps')()
const SentryCliPlugin = require('@sentry/webpack-plugin')

module.exports = withSourceMaps({
  webpack: (config, { isServer, buildId }) => {
    config.plugins.push(
      ...[
        new webpack.DefinePlugin({
          'process.env.SENTRY_RELEASE': JSON.stringify(buildId)
        }),
        new SentryCliPlugin({
          include: ['.next'], // 上传的文件夹,next项目传.next文件夹就行
          ignore: ['node_modules', 'next.config.js'], // 忽略的文件
          configFile: '.sentryclirc', // 上传相关的配置文件
          release: buildId,           // 版本号
          urlPrefix: '~/_next'        // 最关键的,相对路径
        })
      ]
    )
    ...
    return config
  },
})

这里有非常重要的一点,就是urlPrefix这个参数,它默认是~也就是域名加上上传的文件路径,但是 Next.js 项目比特殊,文件路径前面都会加上一个/_next,所以需要加上这个配置项才可以正常访问,要不然 Sentry 获取不到对应的 source-map 文件。

  • 第四步:代码配置
// _app.js
Sentry.init({
  // Replace with your project's Sentry DSN
  dsn: process.env.SENTRY_DSN,
  release: process.env.SENTRY_RELEASE, // 版本号
});

这里同样有个注意点,就是Sentry.init({})的版本号必须与next.config.js的配置项里的版本号一致,这样 Sentry 才可以关联上。在 Next.js 项目中非常简单,使用内置的 buildId 即可~

  • 第五步:build + 查看效果

运行yarn build命令会发现控制台在不断的上传 source-map 文件,如下图所示:

build 过后,在 Sentry 项目控制台可以查看到对应版本的 source-map 文件,如下图所示:

然后重新运行程序,会发现错误堆栈信息已经可以查看源码了,具体那一行代码抛出的异常都可以查看~

扩展项目 —— 用户反馈

关于用户反馈,那就是仁者见仁智者见智了,如果你希望线上项目崩溃的时候显得很优雅,并且你有诉求你的用户能够帮你排查问题,复现步骤,那还是可以的。不过呢,不是所有人都希望这样,因为这就代表着你让用户发现了系统的问题,可能有的人不希望用户发现,或者希望的是私下反馈及时解决避免更多的人发现~总而言之,这也算是一个知识点,就简单配置一下玩耍玩耍:

Sentry.init({
  // Replace with your project's Sentry DSN
  dsn: process.env.SENTRY_DSN,
  beforeSend(event) {
    // Check if it is an exception, if so, show the report dialog
    if (process.browser && event.exception) {
      Sentry.showReportDialog({
        eventId: event.event_id
      });
    }
    return event;
  }
});

其实就一个 API,Sentry.showReportDialog(),我们在出现异常的时候调用它,就可以让用户填写对应的内容进行展示。具体过程如下图所示:

  • 第一步:运行程序出现错误

  • 第二步:填写返回信息

  • 第三步:Sentry 控制用户反馈查看对应信息

从上图可以看出,出现了反馈建议,并且在 Sentry 的反馈列表里,对应反馈内容也正常显示~

扩展项目 —— 关联 Github/Gitlab

继续来扩展项目,那就是 Sentry 监控的错误还可以关联 Github/Gitlab,然后直接新建 Issue,对于测试同学来说,简直是福音啊~

第一次初始化配置我们需要在错误详情页与项目进行关联,如下图所示:

然后安装相应的插件,这里以 Github 为例:

这里是安装不上的,因为这些插件需要在安装 Sentry 的同时一起安装才可以,也就是通过 config.yml 文件配置对应插件。具体的就是上篇文章内容了,因为某些原因(疫情没结束,家里没网络,4G 网络确实有点费劲),这里就不进行安装了,对应地址如下:Sentry 安装 Github Integrations

安装好之后关联 Issue 效果如下:

下面这些截图从网上获取,对应文章 -> 另一篇 Sentry 文章

其实公司项目没有必要这么新建 JIRA,有测试同学以及邮件提醒,所以够用了,不过如果是个人项目,还是挺有用的。感兴趣的可以自行配置一下~不配置也没事,不耽误使用。

扩展项目 —— 错误级别以及邮件设置

在这里承接上文,上面提到了 Fetch Error 是捕获数据请求的异常的,那么如果服务器崩溃了,重启期间,假设有10000个访问,那么开发人员就会受到邮件轰炸,不是很合理,有没有解决办法呢?有~就是邮件提醒设置。

邮件设置不需要任何代码,使用起来也很简单,当然也很重要,具体设置如下图所示:

Project -> Alerts -> Rules -> Edit进行设置。

上面我设置了,只有 error 以上级别的异常才会发邮件,如果是 error 以下的级别(warning/info)就不发邮件了,但是仍然上报。接下来我们就是需要将我们的 Fetch Error 重新设置级别,因为默认上报的就是 error 级别。

// unfetch.js

/* config the fetch error is warning */
Sentry.withScope(function(scope) {
  scope.setLevel('warning');
  Sentry.captureException(err);
});

通过上面的代码,我们将 Fetch Error 设置成了 warning 级别,具体如下图:

  • warning 级别的颜色是橙色,其他正常异常的是红色

  • 错误详情里,本次异常的 level 对应也是 warning

最后重启项目,就会发现,其他异常依然会有邮件警告,而 Fetch Error 已经没有邮件警告了,但是我们依然会在 Sentry 控制台看到它们~很完美!

总结

至此为止,继上篇基础篇之后,实践篇也写完了,基本包含了所有的可用场景。希望对大家有所帮助,前端监控在生产开发过程中还是比较重要的。感兴趣的可以一起交流~

项目地址如下:next-sentry-easy,该项目优化了 Next.js_with-sentry-simple 的 bug,精简了 Next.js_with-sentry 的复杂逻辑,个人感觉算是一个比较平均的项目,如果感兴趣记得 Star,谢谢~

基础代码是master分支,完整代码是full-demo分支。