1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React】error-boundaryでホワイトアウトを抑止しつつ、発生時にCloudwatchにログを残す

Last updated at Posted at 2023-01-16

はじめに

雑なイラストですがやりたいことの解説を。

現状

images_before.png

  • Webアプリ上で、予期しないエラーが発生した場合に、画面が真っ白(ホワイトアウト)になるのを回避したい

    • ReactのError boundaryの機能を使ってエラー発生時に予め用意したエラー画面へ飛ばすようにします。
  • 予期しないエラーが発生した場合に、なぜエラーになったのかを解析できるようにログを残したい

    • ReactなどのSPA Webアプリケーションだと基本的にクライアントの中だけでアプリが動くので、エラーが発生した時の情報は意図的にサーバーに投げない限りは検知できません。
    • そこで、エラーが発生した時にAWSのcloudwatchにログを出力するようにします

理想

images_after.png

error-boudary

ReactにはError boundaryというエラーをいい感じにキャッチして捌いてくれる機能があります。
React 16以降の機能になります。

error boundaryは自身の子コンポーネントツリーで発生したJavaScriptエラーをキャッチし、エラーを記録し、クラッシュしたコンポーネントツリーの代わりにフォールバック用のUIを表示するReactコンポーネントです。error boundaryは配下のツリー全体のレンダー中、ライフサイクルメソッド内、およびコンストラクタ内で発生したエラーをキャッチします。

cloudwatch

AWSはCloudWatch Logsというロギングのサービスを提供しているので、こちらを利用します。

最終的な完成形

ログイン機能からログイン後の画面にて、わざとエラーを仕込んで落とした場合の挙動を確認します。

before

画面

before.gif

  • occure Errorボタンを押したあと、エラーによって画面が真っ白(ホワイトアウト)になっているのがわかります。

after

画面

after.gif

  • occure Errorボタンを押したあと、エラー用の画面に遷移させることができました。

ログ

  • ロググループ
    001_logGroup.png

  • ログストリーム
    002_logStream.png

  • ログ詳細
    003_logDetail.png

エラーになった時のログがCloudwatchに出力できています。

使ったライブラリ・バージョン

viteで構築し、Typescriptベースで実装しました。

ライブラリ バージョン
@aws-amplify/ui-react ^4.3.3
aws-amplify ^4.3.10
aws-amplify-react ^5.1.9
react-error-boundary ^3.1.4
vite ^4.0.0

実装

ベース構築

  • viteでベースとなるReactを構築
yarn create vite

✔ Select a framework: › React
✔ Select a variant: › TypeScript
  • package install
yarn add @aws-amplify/ui-react aws-amplify aws-amplify-react react-error-boundary
  • ログイン画面とログイン後画面の実装

    • App.tsxの元々の画面をpages/Home.tsxとして利用します
  • App.tsx

    • Amplify + cognitoでログイン画面周りを作る実装の詳細は検索すれば色々と記事が出てくるのでそちらを参照(このあたりとか)
App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import { Amplify, Auth } from "aws-amplify";
import moment from "moment";
import { useEffect } from "react";
import Home from "./pages/home";

const App: React.FC = () => {
  useEffect(() => {
    Amplify.configure({
      Auth: {
        identityPoolId: // cognito identityId,
        region: // リージョン,
        userPoolId: // cognito ユーザープールID,
        userPoolWebClientId: // cognito クライアントID,
      },
    });
  }, []);

  return (
    <Authenticator hideSignUp={true}>
      <Home />
    </Authenticator>
  );
};

export default App;
  • pages/home.tsx
    • App.tsxの元々の実装の使い回し
home.tsx
import { useState } from "react";
import reactLogo from "../assets/react.svg";
import "../App.css";

const Home = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </div>
  );
};

export default Home;

この時点でログイン画面と、ログイン後の画面が実装できました。

Error-boundary

ErrorBoundaryの実装については、ErrorBoundary機能のラッパーライブラリでシンプルに実装できるreact-error-boundaryを使います。

  • pages/error.tsx
    • まずエラー時に遷移させる先の画面を作ります。
error.tsx
import { FallbackProps } from "react-error-boundary";

const ErrorFallback = ({ error }: FallbackProps) => {
  return (
    <div>
      <h1>Error-boudanry</h1>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>reload</button>
    </div>
  );
};

export default ErrorFallback;
  • pages/home.tsx
    • ErrorBoundaryの設定と、わざとErrorを発生させるボタンを追加実装します。
home.tsx
import { useState } from "react";
import reactLogo from "../assets/react.svg";
import "../App.css";
+ import { ErrorBoundary } from "react-error-boundary";
+ import ErrorFallback from "./error";

const Home = () => {
  const [count, setCount] = useState(0);
+  const [isError, setError] = useState(false);

+  const onError = (error: Error, info: { componentStack: string }) => {
+    console.log("Error boundary", error.message);
+    console.log("Error boundary", info.componentStack);
+  };


  return (
    <div className="App">
+      <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
        <div>
          <a href="https://vitejs.dev" target="_blank">
            <img src="/vite.svg" className="logo" alt="Vite logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
        <h1>Vite + React</h1>
        <div className="card">
          <button onClick={() => setCount((count) => count + 1)}>
            count is {count}
          </button>
          <p>
            Edit <code>src/App.tsx</code> and save to test HMR
          </p>
+          <button onClick={() => setError((preError) => !preError)}>
+            occure Error
+          </button>
+          {isError ? (
+            <>
+              <p>エラーを起こす</p>
+              <ThrowError />
+            </>
+          ) : (
+            <></>
+          )}
        </div>
        <p className="read-the-docs">
          Click on the Vite and React logos to learn more
        </p>
+      </ErrorBoundary>
    </div>
  );
};

export default Home;

+ function ThrowError(): JSX.Element {
+   throw new Error("manual throw Error");
+ }

修正後はエラー画面に遷移するようになります
after.gif

cloudwatch へのログ出力

cloudwatchへのログの出力はAmplifyの機能を使って実装します。
cognitoに紐付いているIAMロールにcloudwatchへログを書き込む権限を与えるのも忘れないようにします。

実装

  • pages/home.tsx
    • Error発生時(onError発火時)に、cloudwatchへログを書き込むように処理を追加します。
home.tsx
import { useState } from "react";
import reactLogo from "../assets/react.svg";
import "../App.css";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./error";
+ import {
+   Logger as AmplifyLogger,
+   AWSCloudWatchProvider,
+   Auth,
+   Amplify,
+ } from "aws-amplify";

const Home = () => {
  const [count, setCount] = useState(0);
  const [isError, setError] = useState(false);

  const onError = (error: Error, info: { componentStack: string }) => {
    console.log("Error boundary", error.message);
    console.log("Error boundary", info.componentStack);

+    const awsLogger = new AmplifyLogger("log-prefix", "INFO");
+    Amplify.register(awsLogger);
+    awsLogger.addPluggable(new AWSCloudWatchProvider());
+    awsLogger.error("Error boudary", error.message, info.componentStack);
  };

  return (
    <div className="App">
      <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
        <div>
          <a href="https://vitejs.dev" target="_blank">
            <img src="/vite.svg" className="logo" alt="Vite logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
        <h1>Vite + React</h1>
        <div className="card">
          <button onClick={() => setCount((count) => count + 1)}>
            count is {count}
          </button>
          <p>
            Edit <code>src/App.tsx</code> and save to test HMR
          </p>
          <button onClick={() => setError((preError) => !preError)}>
            occure Error
          </button>
          {isError ? (
            <>
              <p>エラーを起こす</p>
              <ThrowError />
            </>
          ) : (
            <></>
          )}
        </div>
        <p className="read-the-docs">
          Click on the Vite and React logos to learn more
        </p>
      </ErrorBoundary>
    </div>
  );
};

export default Home;

function ThrowError(): JSX.Element {
  throw new Error("manual throw Error");
}

AmplifyLoggerの第2引数で、ログのレベル以降を Cloudwatchに出力するかを設定することができます。
今回の例だとINFOを設定しているので、INFOWARNERRORのログがCloudwatch側にも出力されます。

  • App.tsx
    • Cloudwatchへの出力のためのAmplifyの設定を追加します。
App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import "@aws-amplify/ui-react/styles.css";
import { Amplify, Auth } from "aws-amplify";
import moment from "moment";
import { useEffect } from "react";
import Home from "./pages/home";

const App: React.FC = () => {
  useEffect(() => {
    Amplify.configure({
      Auth: {
        identityPoolId: // cognito identityId,
        region: // リージョン,
        userPoolId: // cognito ユーザープールID,
        userPoolWebClientId: // cognito クライアントID,
      },
+      Logging: {
+        // ログの種類ごと
+        logGroupName: `/error-boundary/testlogging`,
+        // ログを出力するアプリケーションのインスタンスごと
+        logStreamName: '/hoge',
+        region: poolConf.region
+      },
    });
  }, []);

  return (
    <Authenticator hideSignUp={true}>
      <Home />
    </Authenticator>
  );
};

export default App;
  • logGroupNameで Cloudwatch上にログを出力する際のロググループを、logStreamNameでログストリーム名を設定しています。

  • Loggingの中にregionを設定しないと以下のようなエラーが出て動作しませんでした。

    react_devtools_backend.js:4012 [ERROR] 06:03.501 AWSCloudWatch - error getting log group - Error: Region is missing
    react_devtools_backend.js:4012 [ERROR] 06:03.502 AWSCloudWatch - failure during log group search: Error: Region is missing
    react_devtools_backend.js:4012 [ERROR] 06:03.502 AWSCloudWatch - failure while getting next sequence token: Error: Region is missing
    react_devtools_backend.js:4012 [ERROR] 06:03.503 AWSCloudWatch - error during _safeUploadLogEvents: Error: Region is missing
    react_devtools_backend.js:4012 [ERROR] 06:03.503 AWSCloudWatch - error when calling _safeUploadLogEvents in the timer interval - Error: Region is missing
    

AWS 側の設定

cognitoに紐付いているIAMロールにCroudwatchへの書き込み権限を与えます。

  • cognito のユーザープールのアプリケーションクライアントの設定
    004_cognito.png

  • 設定されているアプリケーションクライアントと紐付いているIDプール
    005_identityPool.png

  • 認証されたロールで設定している IAM ロールに追加でポリシーをアタッチする
    006_iam_policy.png

    • アタッチするポリシー

      {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
              "logs:CreateLogStream",
              "logs:DescribeLogGroups",
              "logs:DescribeLogStreams",
              "logs:CreateLogGroup",
              "logs:PutLogEvents"
            ],
            "Resource": "*"
          }
        ]
      }
      

まとめ

  • Reactの中でエラーが発生した場合に自動的にキャッチしてエラー画面に遷移すること。
  • エラー画面に遷移した場合にエラーの内容をCloudwatchへ連携すること。

が実装できました。

このCloudwatchに関する実装は、今回はエラーの部分だけに適用しましたが、
例えばユーザのボタンクリックなどの操作毎や、画面遷移毎といったユーザ操作のトラッキングを目的としてのCloudwatchへの出力にも応用できます。
console.logを使うように、簡単にSPAでユーザ操作のトラッキングが行うことができるので中々良いと思います。

参考

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?