🐱

Next.js + VercelとSentryを連携させる

2022/05/01に公開

概要

Next.js + Vercelで運用しているアプリケーションにSentryを連携させる手順を説明します。

今回は実際に自分が 友人 と開発・運用しているサービス LGTMeow にこれらの設定を追加したので、実際の設定内容を踏まえて解説します。

https://lgtmeow.com

対象読者

Next.jsのアプリケーションをVercelにデプロイしている人を対象読者としています。

事前準備

Sentryへサインアップ

以下のページからサインアップを行ないます。

https://sentry.io/signup/

自分はGitHubのアカウントを利用して作成しました。

Sentry organizationの作成

以下のページより作成します。

https://sentry.io/organizations/new/

自分はGitHub Organizationと同じ名称で作成しました。

Sentry プロジェクトの作成

プロジェクトを作成します。

create-project

  • プラットフォームを選択でNext.jsを選択
  • Set your default alert settingsはとりあえず I'll create my own alerts later を選択
  • プロジェクト名は何でも良いですが自分はGitHubリポジトリと同じ名前にしました

create-project2

アプリケーションにSentryの設定をする

左側の「プロジェクト」から作成したプロジェクトを確認できるかと思います。

create-project3

「インストール方法」を押下すると「あなたのアプリケーションを設定」という画面が出るのでNext.jsを選択します。

setup1

Next.jsを選択すると以下のようにインストール方法が表示されます。

configure-next-js

ここに載っている内容に従いpackageのインストールや設定ファイルの修正などを行なっていきます。

@sentry/nextjs のインストールと設定

以下を実行します。

npm install --save @sentry/nextjs

Sentry wizardの実行

以下を実行します。

npx @sentry/wizard -i nextjs

この時にブラウザで以下のページが開かれます。

おそらくですがSentryの認証エンドポイントに移動してトークンを取得しているのではないかと思います。(未確認)

browser

ターミナル上で以下のように利用したいプロジェクトを選択する画面になるので選択します。

SentryWizard1

プロジェクトを選択するとSentry wizardは完了します。

SentryWizard2

さまざまなファイルが生成・修正されます。

Sentry wizardによって追加されるファイル

sentry.client.config.js
// This file configures the initialization of Sentry on the browser.
// The config you add here will be used whenever a page is visited.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from '@sentry/nextjs';

const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

Sentry.init({
  dsn: SENTRY_DSN || 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@o1111111.ingest.sentry.io/0000000',
  // Adjust this value in production, or use tracesSampler for greater control
  tracesSampleRate: 1.0,
  // ...
  // Note: if you want to override the automatic release value, do not set a
  // `release` value here - use the environment variable `SENTRY_RELEASE`, so
  // that it will also get attached to your source maps
});
sentry.server.config.js
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from '@sentry/nextjs';

const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;

Sentry.init({
  dsn: SENTRY_DSN || 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@o1111111.ingest.sentry.io/0000000',
  // Adjust this value in production, or use tracesSampler for greater control
  tracesSampleRate: 1.0,
  // ...
  // Note: if you want to override the automatic release value, do not set a
  // `release` value here - use the environment variable `SENTRY_RELEASE`, so
  // that it will also get attached to your source maps
});
sentry.properties
defaults.url=https://sentry.io/
defaults.org=あなたのSentry organization名
defaults.project=あなたのSentryプロジェクト名
cli.executable=../../.npm/_npx/xxxxxxxxxxxxxxxx/node_modules/@sentry/cli/bin/sentry-cli
next.config.js
// This file sets a custom webpack configuration to use your Next.js app
// with Sentry.
// https://nextjs.org/docs/api-reference/next.config.js/introduction
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

const { withSentryConfig } = require('@sentry/nextjs');

const moduleExports = {
  // Your existing module.exports
};

const sentryWebpackPluginOptions = {
  // Additional config options for the Sentry Webpack plugin. Keep in mind that
  // the following options are set automatically, and overriding them is not
  // recommended:
  //   release, url, org, project, authToken, configFile, stripPrefix,
  //   urlPrefix, include, ignore

  silent: true, // Suppresses all logs
  // For all available options, see:
  // https://github.com/getsentry/sentry-webpack-plugin#options.
};

// Make sure adding Sentry options is the last code to run before exporting, to
// ensure that your source maps include changes from all other Webpack plugins
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions);
src/pages/_error.js
import NextErrorComponent from 'next/error';

import * as Sentry from '@sentry/nextjs';

const MyError = ({ statusCode, hasGetInitialPropsRun, err }) => {
  if (!hasGetInitialPropsRun && err) {
    // getInitialProps is not called in case of
    // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
    // err via _app.js so it can be captured
    Sentry.captureException(err);
    // Flushing is not required in this case as it only happens on the client
  }

  return <NextErrorComponent statusCode={statusCode} />;
};

MyError.getInitialProps = async (context) => {
  const errorInitialProps = await NextErrorComponent.getInitialProps(context);
  
  const { res, err, asPath } = context;

  // Workaround for https://github.com/vercel/next.js/issues/8592, mark when
  // getInitialProps has run
  errorInitialProps.hasGetInitialPropsRun = true;

  // Returning early because we don't want to log 404 errors to Sentry.
  if (res?.statusCode === 404) {
    return errorInitialProps;
  }
  
  // Running on the server, the response object (`res`) is available.
  //
  // Next.js will pass an err on the server if a page's data fetching methods
  // threw or returned a Promise that rejected
  //
  // Running on the client (browser), Next.js will provide an err if:
  //
  //  - a page's `getInitialProps` threw or returned a Promise that rejected
  //  - an exception was thrown somewhere in the React lifecycle (render,
  //    componentDidMount, etc) that was caught by Next.js's React Error
  //    Boundary. Read more about what types of exceptions are caught by Error
  //    Boundaries: https://reactjs.org/docs/error-boundaries.html

  if (err) {
    Sentry.captureException(err);

    // Flushing before returning is necessary if deploying to Vercel, see
    // https://vercel.com/docs/platform/limits#streaming-responses
    await Sentry.flush(2000);

    return errorInitialProps;
  }

  // If this point is reached, getInitialProps was called without any
  // information about what the error might be. This is unexpected and may
  // indicate a bug introduced in Next.js, so record it in Sentry
  Sentry.captureException(
    new Error(`_error.js getInitialProps missing data at path: ${asPath}`),
  );
  await Sentry.flush(2000);

  return errorInitialProps;
};

export default MyError;
.sentryclirc
[auth]
token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

next.config.js について

プロジェクトルートにすでに next.config.js が存在する場合は next.config.wizardcopy.js という名前で生成されます。

ほとんどのプロジェクトでは next.config.js がすでに存在していると思いますので大抵の場合は next.config.wizardcopy.js が生成されます。

この場合は既存の next.config.js とマージしてあげる必要があります。

筆者の環境の場合 next.config.js は以下のようになりました。

next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

/**
 * @type {import('next').NextConfig}
 */
const moduleExports = {
  images: {
    domains: ['lgtm-images.lgtmeow.com', 'stg-lgtm-images.lgtmeow.com'],
  },
  swcMinify: true,
};

const sentryWebpackPluginOptions = {
  silent: true,
};

module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions);

筆者は未確認ですが withSentryConfig 以外に複数のプラグインを利用している場合は next-compose-plugins を利用すると良さそうです。

参考になりそうな記事を貼っておきます。

(参考記事)Next.js / Sentry / Vercel の連携が楽になっていた件

src/pages/_error.js について

筆者の環境だと src/pages/_error.tsx を作成していました。

その為生成された src/pages/_error.js の内容をマージしつつTypeScriptの対応も行なう必要がありました。

asを使って強引に型解決している部分がありますが、ひとまずこれで動作しました。

src/pages/_error.tsx
import * as Sentry from '@sentry/nextjs';
import { NextPage, NextPageContext } from 'next';
import NextErrorComponent from 'next/error';

import { httpStatusCode, HttpStatusCode } from '../constants/httpStatusCode';

type Props = {
  statusCode: HttpStatusCode;
  err?: Error;
  hasGetInitialPropsRun?: boolean;
};

const CustomErrorPage: NextPage<Props> = ({
  statusCode,
  hasGetInitialPropsRun,
  err,
}) => {
  if (!hasGetInitialPropsRun && err) {
    Sentry.captureException(err);
  }

  return <NextErrorComponent statusCode={statusCode} />;
};

const defaultTimeout = 2000;

CustomErrorPage.getInitialProps = async (
  context: NextPageContext,
): Promise<Props> => {
  const errorInitialProps = (await NextErrorComponent.getInitialProps(
    context,
  )) as Props;

  const { res, err, asPath } = context;

  errorInitialProps.hasGetInitialPropsRun = true;

  if (res?.statusCode === httpStatusCode.notFound) {
    return errorInitialProps;
  }

  if (err) {
    Sentry.captureException(err);

    await Sentry.flush(defaultTimeout);

    return errorInitialProps;
  }

  Sentry.captureException(
    new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`),
  );
  await Sentry.flush(defaultTimeout);

  return errorInitialProps;
};

export default CustomErrorPage;

余談ですが import { httpStatusCode, HttpStatusCode } from '../constants/httpStatusCode'; の部分は以下のようにHTTPステータスコードを表す内容です。

src/constants/httpStatusCode.ts
// https://developer.mozilla.org/ja/docs/Web/HTTP/Status から必要なものを抜粋して定義
export const httpStatusCode = {
  ok: 200,
  created: 201,
  accepted: 202,
  noContent: 204,
  movedPermanently: 301,
  badRequest: 400,
  unauthorized: 401,
  forbidden: 403,
  notFound: 404,
  requestTimeout: 408,
  unprocessableEntity: 422,
  internalServerError: 500,
  serviceUnavailable: 503,
} as const;

export type HttpStatusCode = typeof httpStatusCode[keyof typeof httpStatusCode];

Sentry wizardによって更新されるファイル

.gitignore に以下が追加されます。

.sentryclirc には機密情報であるトークンが記載される事になるので、それをコミットしてしまわないような配慮だと思われます。

.gitignore
# Sentry
.sentryclirc

Next.js API Routeについて

Next.jsのAPI Routeを利用している場合は以下のように withSentry の引数に handler 関数を渡すように修正します。

これでAPI Route内で発生したErrorもSentryに通知されるようになります。

import type { NextApiRequest, NextApiResponse } from "next"
import { withSentry } from "@sentry/nextjs";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  res.status(200).json({ name: "John Doe" });
};

export default withSentry(handler);

Sentryにエラーが送られているかローカルで動作確認

適当なボタンを押下した際に例外を発生するようにして、アプリケーションサーバーを起動します。

左メニューの「課題」を確認するとエラーが送信されていることが確認できます。

issues

詳細を確認するとエラー発生時のブラウザの情報やIPアドレスなどが確認できます。

ErrorDetail1

ErrorDetail2

VercelとSentryの連携

Sentry Integrationを利用するのがもっとも簡単なのでそも手順を実施します。

https://docs.sentry.io/product/integrations/deployment/vercel/

  1. Visit https://vercel.com/integrations/sentry/add

こちらに従い https://vercel.com/integrations/sentry/add に移動して「Add Integration」を押下します。

AddIntegration1

追加するVercelのTeamsを選択します。

AddIntegration2

ここでは必要なVercelプロジェクトだけを選択します。

AddIntegration3

Install Vercelを押下します。

AddIntegration4

VercelのプロジェクトとSentryのプロジェクトを連携させて「Complete on Vercel」を押下します。

AddIntegration5

これで完了です。Vercel上の環境変数を確認するとSentryが追加した環境変数が確認できます。

必要な環境変数はこれで問題はないのですが Production でしか読めない設定になっています。

必要に応じて Preview, Development でも読めるように設定を変更しておきましょう。

Vercel

Vercel上で動作確認

Sentryの動作に必要な環境変数をSentry Integrationによって追加したので各設定ファイルも環境変数から必要な値をロードするように変更します。

next.config.js の修正

authToken, org, project を環境変数からロードするように変更します。

next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

/**
 * @type {import('next').NextConfig}
 */
const moduleExports = {
  // 任意の設定
};

const sentryWebpackPluginOptions = {
  silent: true,
  authToken: process.env.SENTRY_AUTH_TOKEN,
  org: process.env.SENTRY_ORG,
  project: process.env.SENTRY_PROJECT,
};

module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions);

sentry.client.config.js, sentry.server.config.js

内容はどちらも同じです。

dsn をVercelに登録されている NEXT_PUBLIC_SENTRY_DSN から参照するように変更します。

それから environment を設定しておきます。

これを設定しておくとSentry上からどの環境で起こったかわかりやすくなるのでオススメです。

筆者は NEXT_PUBLIC_APP_ENV という環境変数を以下のように定義しています。

  • Vercel上でDevelopmentの場合は local を指定、ローカル環境で発生したErrorと認識するため
  • Vercel上でPreviewの場合は development を指定、Vercel上の開発環境で発生したErrorと認識するため
  • Vercel上でProductionの場合は production を指定、Vercel上の本番環境で発生したErrorと認識するため

↓のようにフィルターが可能になったり通知の際に本番環境で起こったものだけを通知するなどの対応が可能になるので便利です。

Filter

sentry.client.config.js
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_APP_ENV,
  tracesSampleRate: 1.0,
});
sentry.server.config.js
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_APP_ENV,
  tracesSampleRate: 1.0,
});

sentry.properties を削除する

authToken, org, project を環境変数からロードするようにしたので、このファイルに関しては削除してしまって問題ありません。

Vercel上にデプロイ後に動作確認

以下のようにVercel上で発生したErrorも通知されていることが確認できます。

VercelError

おわりに

@sentry/nextjs を利用したSentryの設定を説明しました。

環境変数の追加を忘れて動作しなかったりハマった部分もあったので記事にまとめてみました。

この記事は入門向きですが、さらに実践的な内容を解説した Next.jsにSentryを導入した際の課題と解決策について という記事を教えてもらったので参考リンクとして載せておきます。

この記事でSentryを導入した後に読むのがオススメです。

https://note.com/tabelog_frontend/n/n7f6822ae0c0d

以上になります。最後まで読んでいただきありがとうございました。

Discussion