【CloudWatch】アプリケーションのユーザ離脱を監視する【カスタムメトリクス】

# やったこと
- Webアプリケーションのユーザ離脱数をCloudWatchカスタムメトリクスとして取得する
- ユーザ離脱は、ユーザが異なるページに遷移しようとする「beforeunload」イベントを監視することで簡易的に検知
- 「beforeunload」イベント発生時に、正常に漏れなくCloudWatchにデータを送信できるよう、Beacon APIを使用する。

# 構成図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/e95e6f5a-d8ab-0ec9-4fc3-29d993edf112.png)

# バックエンド
一般的なAPI GatewayとLambdaの構成をとる。
API GatewayにPOSTメソッドを作成し、LambdaプロキシでLambdaと紐づける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/2aeda944-e929-4b81-a7d4-c958a39bf9f9.png)

Lambdaにはデフォルト権限に加えて「cloudwatch:PutMetricData」権限を付与する。
```
{
    "Effect": "Allow",
    "Action": "cloudwatch:PutMetricData",
    "Resource": "*"
}
```

ソースコードは以下の通り。

```index.js
import { CloudWatchClient, PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";

const cloudwatch = new CloudWatchClient({ region: 'ap-northeast-1'});

export const handler = async (event) => {
  try {
    const data = JSON.parse(event.body);
    const params = {
      MetricData: [
          {
            MetricName: data.MetricName,
            Dimensions: data.Dimensions,
            Value: data.Value,
            Unit: 'Count'
          }
      ],
      Namespace: 'UserWithdrawalDetection'
    };

    const command = new PutMetricDataCommand(params);
    await cloudwatch.send(command);
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        msessage: 'Success',
      })
    };
  } catch (error) {
   console.log("Error", error) ;
   return {
      statusCode: 500,
      body: JSON.stringify({
        msessage: 'Failed',
      })
    };
  }
};
```


# フロントエンド（React on S3）
簡易検証のため、srcディレクトリに以下のコンポーネントファイルを配置。

endpointは作成したAPI Gatewayのエンドポイントに書き換える。
```app.js
import './App.css'
import Home from './Home'
import About from './About'
import MyPage from './MyPage'
import UserWithdrawalDetectionComponent from './UserWithdrawalDetectionComponent'
import { BrowserRouter, Route, Routes, Link } from 'react-router-dom'

function App() {
  UserWithdrawalDetectionComponent((event) => {
    const pageUrl = window.location.href
    const data = {
      MetricName: 'PageUnload',
      Dimensions: [
        {
          Name: 'PageUrl',
          Value: pageUrl,
        },
      ],
      Value: 1,
    }

    // endpointは作成したAPI Gatewayのエンドポイントに書き換える
    const endpoint =
      'https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prd/collect-metrics'

    // Becon APIを利用して、データをバックエンド（API Gateway+Lambda）に送付
    navigator.sendBeacon(endpoint, JSON.stringify(data))

    // 検証のため、タブクローズ時に、アサーションを出している
    event.preventDefault()
    event.returnValue = 'Are you sure you want to leave?'
  })
  return (
    <>
      <div className="App">
        <BrowserRouter>
          <Link to="/">Home</Link> | <Link to="/about">About</Link>|{' '}
          <Link to="/myPage">MyPage</Link>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/myPage" element={<MyPage />} />
            <Route path="*" element={<h1>Not Found Page</h1>} />
          </Routes>
        </BrowserRouter>
      </div>
    </>
  )
}

export default App
```
```Home.js
import React from 'react'

const Home = () => {
  return <h1>Home</h1>
}

export default Home
```
```Home.js
import React from 'react'

const Home = () => {
  return <h1>Home</h1>
}

export default Home
```
```MyPage.js
import React from 'react'

const MyPage = () => {
  return <h1>MyPage</h1>
}

export default MyPage
```
```About.js
import React from 'react'

const About = () => {
  return <h1>About</h1>
}

export default About
```
```UserWithdrawalDetectionComponent.js
import { useEffect } from 'react'

const UserWithdrawalDetectionComponent = (callback) => {
  useEffect(() => {
    window.addEventListener('beforeunload', callback)

    return () => {
      window.removeEventListener('beforeunload', callback)
    }
  }, [callback])
}

export default UserWithdrawalDetectionComponent
```
ローカルでnpm run buildコマンドを実行して、生成されたbuildディレクトリ内のファイル、フォルダをS3に配置する。
S3は静的Webホスティング機能を使用する。

# 動作確認
Webアプリケーションにアクセスし、ページごとにタブを閉じる、キャンセルを繰り返す。
この裏側で、どのページでサイトを閉じたのかの情報をCloudWatchに送っている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/e435948a-8c4b-670a-be79-e8849996584c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/df22fd10-277d-ad8a-7a2f-5602b8a5ca7c.png)

CloudWatchメトリクスの確認。
ページURLをディメンションとして、カスタムメトリクスが作成されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/baf4401c-4b4e-8fd0-8284-db5ab27cedd4.png)
ダッシュボードにて期間内のページごとのユーザ離脱数を可視化してみる。
値は各ディメンションごとの合計値を算出している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/992414/0a09d89b-5b1b-60d5-58c1-cb29f3d7b52e.png)

# 最後に
ユーザ離脱率を取得しましたが、加えてユーザアクセス数も取得することでユーザ離脱率まで簡単に取得できそうですね。
サードパーティツールやAWS Pinpointを使用することで簡単に離脱率は取得できますが、そこまでリッチな機能はいらないといったケースでは有効活用できそうです。