CloudWatch RUM がJavaScript ソースマップをサポートしたので試してみた

# はじめに

https://aws.amazon.com/jp/about-aws/whats-new/2025/03/cloudwatch-rum-javascript-source-maps-error-debugging/

2025年 3月 18日に **CloudWatch RUM が JavaScript ソースマップをサポートするようになり、エラーのデバッグが容易になりました** というアップデートがありました。

*アップデートの内容*
*開発者が実際のユーザーインタラクションをモニタリングし、ウェブアプリケーションのフロントエンドパフォーマンスの問題を診断するのに役立つ Amazon CloudWatch RUM が JavaScript ソースマップをサポートするようになりました。これにより、開発者はスタックトレース内の圧縮された JavaScript エラーを読み取り可能な形式に変換して、エラーをより迅速に解決できます。この機能により、フロントエンドの開発者と DevOps チームは、検索可能で人間が判読できる JS エラーを表示し、元のソースコード内のエラーの正確な場所をすばやく特定できるようになりました。
JavaScript エラーは、スタックトレースで圧縮されているとデバッグが難しくなり、問題の原因を特定するのが難しくなります。現在では、本番環境でエラーが発生した場合、RUM は顧客がアップロードしたソースマップを利用して元のコードまでさかのぼって追跡します。RUM イベント内の簡略化されていないスタックトレースを検索する機能が追加されたことで、開発者は傾向を分析し、複数のセッションにわたる問題を相互に関連付けることができるため、繰り返し発生するエラーの検出と優先順位付けを迅速に行うことができます。JavaScript スタックトレースのエラーを減らすには、お客様が App Monitor 設定で有効にし、コンソールまたは RUM API を介してソースマップを保持するバケットまたはフォルダーの S3 URI を指定する必要があります。*

早速試してみました。

# RUM サービスアクセスを許可するように Amazon S3 バケットのリソースポリシーを設定する
ソースマップをアップロードする S3 バケットに対し、 CloudWatch RUM がファイルを取得できる必要があります。そのため、ソースマップをアップロードする S3 バケット に以下のようにバケットポリシーを追加します。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RUM Service S3 Read Permissions",
            "Effect": "Allow",
            "Principal": {
                "Service": "rum.amazonaws.com"
            },
            "Action": [
                "s3:GetObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME",
                "arn:aws:s3:::BUCKET_NAME/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "ACCOUNT_ID",
                    "aws:SourceArn": "arn:aws:rum:REGION:ACCOUNT_ID:appmonitor/APP_MONITOR_NAME"
                }
            }
        }
    ]
}
```


# ソースマップをアップロード
生成したソースマップファイルはS3 バケットにアップロードする必要があります。
例えば、ビルドの結果が下記のような場合
```
./dist
    |-index.d5a07c87.js
    |-index.d5a07c87.js.map
```
下記のようにアップロードする必要があります。
```
my-application-source-maps  <-- バケット名
    |-2.0.0   <--- RELEASE_ID
        |-index.d5a07c87.js.map  <--- ファイル
```
ここで「2.0.0」は RELEASE_ID で CloudWatch RUM ウェブクライアントでも指定する必要があるため、注意が必要です。
RELEASE_ID に関してはユニークな文字列であれば良いです。例えば、コミットIDなどが適切そうです。もし、コミットIDがない場合はUUIDなどを生成してください

## CodeBuildに追加
今回、デプロイにはCodeBuildを利用しているため、buildspec.yml にソースマップのアップロードを追加します。
以下はドキュメントのサンプルロジックです。
```
#!/bin/bash
# Ensure the script is called with required arguments
if [ "$#" -ne 2 ]; then
 echo "Usage: $0 S3_BUCKET_NAME RELEASE_ID"
 exit 1
fi

# Read arguments
S3_BUCKET="$1"
RELEASE_ID="$2"

# Set the path to your build directory
BUILD_DIR="./dist"


# Upload all .map files recursively
 if aws s3 cp "$BUILD_DIR" "s3://$S3_BUCKET/$RELEASE_ID/" --recursive --exclude "*" --include "*.map"; then
    echo "Successfully uploaded all source map files"
else
    echo "Failed to upload source map files"
fi

```

ちなみに、リリースIDでコミットハッシュ値を利用したい場合は`COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)`を使用できます。
今回はこのコミットハッシュを利用します。

実際に利用したymlは以下です。
```
- NUXT_PUBLIC_RUM_RELEASE_ID=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- yarn install
- aws s3 cp ".output/public" "$S3_SOURCEMAP_URL/$NUXT_PUBLIC_RUM_RELEASE_ID/" --recursive --exclude "*" --include "*.map"
```

# CloudWatch RUM ウェブクライアントで ReleaseID を設定
RUM Config に `releaseId`を追加します。
ビルドごとに値を変えるために、環境変数を利用します。
このため、CodeBuild の buildspex.yml で環境変数を`NUXT_PUBLIC_RUM_RELEASE_ID`を設定する必要があります。
今回は`NUXT_PUBLIC_RUM_RELEASE_ID=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)`のようにコミットハッシュを利用しました。

```
const config: AwsRumConfig = {
      sessionSampleRate: 1,
      identityPoolId: runtimeConfig.public.rumIdentityPoolId,
      endpoint: runtimeConfig.public.rumEndpoint,
      telemetries: ["performance", "errors", ['http', { addXRayTraceIdHeader: true, recordAllRequests: true, }]],
      allowCookies: true,
      enableXRay: true,
      sessionEventLimit: -1,
      releaseId: runtimeConfig.public.rumReleaseId
    };
```

# Nuxt3 で Source Map を作成する
`nuxt.config.ts` に
```
sourcemap: {
    server: true,
    client: true
  },
```
を追加し、今までと同様にビルドを行うことで、`.js.map` ファイルが生成されます。

https://nuxt.com/docs/api/nuxt-config#sourcemap

ここまでできたら、まずはビルドを行い、S3にソースマップがアップロードされることをチェックします。

![スクリーンショット 2025-03-24 11.41.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/10be987f-0a69-480a-bae5-97a27f79f98c.png)

問題なさそうです。

# CloudWatch RUM のAPP MONITOR で設定
Configration タブから `Unminify JavaScript error stack traces` のチェックをつけ、Source map を保存したS3バケットを指定します。

![スクリーンショット 2025-03-24 10.38.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1005574/308fb62b-77c5-4140-bbb6-5febbfd0449a.png)

# 動作確認
JSエラーを発生させて、CloudWatch RUM のコンソールのエラーイベントを確認してみます。

```
"event_details": {
    "version": "1.0.0",
    "type": "TypeError",
    "message": "Cannot read properties of undefined (reading 'forEach')",
    "filename": null,
    "lineno": null,
    "colno": null,
    "stack": "TypeError: Cannot read properties of undefined (reading 'forEach')\n    at setup (https://d2qi37f0jf8y2l.cloudfront.net/_nuxt/Cbabe4kd.js:1:1008)",
    "unminifiedStack": "TypeError: Cannot read properties of undefined (reading 'forEach')\n    at setup (../../../../pages/index.vue:85:17)"
  }
```
**unminifiedStack** が追加されており、**at setup (../../../../pages/index.vue:85:17)** のように圧縮前のJSファイルのどこでエラーが発生したかが分かります。
また、圧縮前のコードは下記のため、正しい箇所が指摘されています。
```
85:    res.pictures.forEach(element => {
86:        pictures.value.push(element)
87:    })
```
エラーが起きた時に圧縮されたファイルで頑張ってエラー箇所を特定する必要がなくなるので、かなり楽になりそうです！
なお、スタックトレース内に **[as default]** や **[as fn]** がある場合には、
```
UnminifyLineFailure: Source map file 0cec3b1/[as default] https://xxx.cloudfront.net/_nuxt/DCLV2syQ.js.map not found at yyy(S3バケット名) for accountId zzz.\n
UnminifyLineFailure: Source map file 0cec3b1/[as fn] https://xxx.cloudfront.net/_nuxt/DCLV2syQ.js.map not found at yyy(S3バケット名) for accountId zzz.\n    
```
のように上手く解消できないようでした。
この点は、修正されたらこの記事も修正しておきます。


# まとめ
ClouWatch RUM で JavaScript エラー時に JavaScript ソースマップからスタックトレース内の圧縮された JavaScript エラーを読み取り可能な形式に変換して、表示されることを確認しました。
この機能を利用することで、エラー発生時に問題箇所を素早く特定できるようになります。
とても便利な機能だったので、ぜひお試しください。