はじめに
雑なイラストですがやりたいことの解説を。
現状
-
Webアプリ上で、予期しないエラーが発生した場合に、画面が真っ白(ホワイトアウト)になるのを回避したい
- Reactの
Error boundary
の機能を使ってエラー発生時に予め用意したエラー画面へ飛ばすようにします。
- Reactの
-
予期しないエラーが発生した場合に、なぜエラーになったのかを解析できるようにログを残したい
- ReactなどのSPA Webアプリケーションだと基本的にクライアントの中だけでアプリが動くので、エラーが発生した時の情報は意図的にサーバーに投げない限りは検知できません。
- そこで、エラーが発生した時にAWSのcloudwatchにログを出力するようにします
理想
error-boudary
ReactにはError boundary
というエラーをいい感じにキャッチして捌いてくれる機能があります。
React 16以降の機能になります。
error boundaryは自身の子コンポーネントツリーで発生したJavaScriptエラーをキャッチし、エラーを記録し、クラッシュしたコンポーネントツリーの代わりにフォールバック用のUIを表示するReactコンポーネントです。error boundaryは配下のツリー全体のレンダー中、ライフサイクルメソッド内、およびコンストラクタ内で発生したエラーをキャッチします。
cloudwatch
AWSはCloudWatch Logsというロギングのサービスを提供しているので、こちらを利用します。
最終的な完成形
ログイン機能からログイン後の画面にて、わざとエラーを仕込んで落とした場合の挙動を確認します。
before
画面
-
occure Error
ボタンを押したあと、エラーによって画面が真っ白(ホワイトアウト)になっているのがわかります。
after
画面
-
occure Error
ボタンを押したあと、エラー用の画面に遷移させることができました。
ログ
エラーになった時のログが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でログイン画面周りを作る実装の詳細は検索すれば色々と記事が出てくるのでそちらを参照(このあたりとか)
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
の元々の実装の使い回し
-
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
- まずエラー時に遷移させる先の画面を作ります。
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を発生させるボタンを追加実装します。
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");
+ }
cloudwatch へのログ出力
cloudwatchへのログの出力はAmplifyの機能を使って実装します。
cognitoに紐付いているIAMロールにcloudwatchへログを書き込む権限を与えるのも忘れないようにします。
実装
- pages/home.tsx
- Error発生時(
onError
発火時)に、cloudwatchへログを書き込むように処理を追加します。
- Error発生時(
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
を設定しているので、INFO
、WARN
、ERROR
のログがCloudwatch側にも出力されます。
- App.tsx
- Cloudwatchへの出力のためのAmplifyの設定を追加します。
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への書き込み権限を与えます。
-
認証されたロール
で設定している IAM ロールに追加でポリシーをアタッチする
-
アタッチするポリシー
{ "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でユーザ操作のトラッキングが行うことができるので中々良いと思います。