【Kotlin】GA4のアプリストリームにWebViewからイベントを送り検証する

AndroidアプリのWebView内から発生させたイベントをネイティブ側のFirebase向けGoogleアナリティクス（あるいはApp+Webプロパティ、GA4）にて計測するための方法。

[Googleの公式ヘルプガイド](https://firebase.google.com/docs/analytics/webview)にandroid向けにはJavaのサンプルコードが記載されていたもののKoltlin版が無かったので作成しました。




# 1. Android プロジェクトに Firebase SDKを導入
以下リンクの「オプション1」の手順通りにやればらくらく。
https://firebase.google.com/docs/android/setup
（iOSアプリの場合はこちら : https://firebase.google.com/docs/ios/setup ）

最後の手順の「アプリを実行してインストールを確認」が何度リトライしても上手く行きませんが大抵スキップして翌日チェックしたら成功してます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/ba5acc79-1d17-e76a-61a6-547d199751b5.png)

# 2. FirebaseプロジェクトをGoogleアナリティクスに接続
以下の手順でFirebaseプロジェクトをGoogleアナリティクスに接続します。

1. 歯車アイコン > プロジェクトを設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/373160af-5822-a229-18d2-a68a2758ce0f.png)

2. 「統合」タブ > Google Analytics 「有効にする」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/17c95e3a-ac35-3b90-c368-35e3b35f8a50.png)

3. プルダウンメニューから選択し、既存のアカウントを選択してGA4プロパティを追加するか、新規にGoogleアナリティクスアカウントを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/fd5b86a0-b272-a56b-487a-be6b237a7cdc.png)

# 3. Webサイト側にJavaScript ハンドラを実装
以下Googleヘルプガイド記載のJavaScriptコードをWebViewで開かれる可能性があるすべてのWebページに実装することで、 [logEvent()](https://firebase.google.com/docs/analytics/events?platform=android&hl=ja) と[setUserProperty()](https://firebase.google.com/docs/analytics/user-properties?platform=android&hl=ja)をJavaScriptから使えるようになります。

## JavaScript コード
Googleヘルプガイドのスクリプトをそのまま加工せずにご利用ください。

Android・iOS共通。アプリ側でネイティブインターフェースが実装されていない場合は`console.log("No native APIs found.");`が実行されるため、User Agentによるアプリ判定も特別な理由がない限り実装不要です。
→ https://firebase.google.com/docs/analytics/webview?platform=android#implement-javascript-handler
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/2b4004c9-46d6-ce5b-0018-1d6ba0b3aeeb.png)
## サイトへのlogEvent() 実装例
FirebaseアナリティクスのJavaScriptハンドラを実装・更にアプリ側に後述のネイティブインターフェースを実装した後、あるボタンのタップ数をFirebaseで計測したい場合、以下のようなコードでWebView内で発生したイベントをGA4に送信できるようになります。

```html
<button type="button" onclick="logEvent('tap_button',{'param_foo':'key_bar','param_baz':'key_qux'})">logEvent Test Button</button>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/9bfdf857-d9d1-bb84-0196-54f6813ab1b2.png)

取得したいイベントがGA4で推奨イベントとして用意されているものであれば可能なかぎり推奨イベント名とGoogle指定のパラメータに従います。

- [イベント: すべてのプロパティ - アナリティクス ヘルプ](https://support.google.com/analytics/answer/9267735)
- [アプリ + ウェブのイベント  |  グローバル サイトタグ（gtag.js）  |  Google Developers](https://developers.google.com/gtagjs/reference/aw-events)

独自のイベント名、パラメータ名を設定しても問題ありませんが、イベント名は40文字以下の半角英数字・アンダースコアで構成され、アルファベットから始まり、パラメータの値は100文字以内である必要があります。
また、推奨イベント・推奨パラメータが[`add_payment_info`](https://developers.google.com/gtagjs/reference/ga4-events#add_payment_info), `item_id`のように命名されているため、独自のイベントなどを定義する場合もスネークケース（小文字の英語をアンダースコアで繋ぐ記述）を使った方が整合性が保てるのでオススメです。

その他の制限事項 → [Sending events  |  Measurement Protocol for Google Analytics 4](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#limitations)

# 4. アプリ側にネイティブ インターフェースを実装
[ヘルプガイド](https://developers.google.com/analytics/devguides/collection/firebase/android/webview#implement_native_interface)にKotlin版が無かったため、Javaのコードを変換して作成しています。

## AnalyticsWebInterfaceクラスを実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/07ef456b-2ba7-bcbd-0ae3-40e4e5e3de69.png)

### Kotlin コード

```kotlin
/** Instantiate the interface and set the context  */
class AnalyticsWebInterface (context: Context) {
    companion object{
        const val TAG = "AnalyticsWebInterface"
    }
    private val firebaseAnalytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(context)
    @JavascriptInterface
    fun logEvent(name: String, jsonParams: String) {
        LOGD("logEvent:$name")
        firebaseAnalytics.logEvent(name, bundleFromJson(jsonParams))
    }

    @JavascriptInterface
    fun setUserProperty(name: String, value: String?) {
        LOGD("setUserProperty:$name")
        firebaseAnalytics.setUserProperty(name, value)
    }

    private fun LOGD(message: String) {
        // Only log on debug builds, for privacy
        if (BuildConfig.DEBUG) {
            Log.d(TAG, message)
        }
    }

    private fun bundleFromJson(json: String): Bundle? {
        if (TextUtils.isEmpty(json)) {
            return Bundle()
        }
        val result = Bundle()
        try {
            val jsonObject = JSONObject(json)
            val keys = jsonObject.keys()
            while (keys.hasNext()) {
                val key = keys.next()
                val value = jsonObject[key]
                when(value){
                    is String -> result.putString(key, value)
                    is Int -> result.putInt(key, value)
                    is Double -> result.putDouble(key, value)
                    else -> Log.w(TAG, "Value for key $key not one of [String, Integer, Double]")
                }
            }
        } catch (e: JSONException) {
            Log.w(TAG, "Failed to parse JSON, returning empty Bundle.", e)
            return Bundle()
        }
        return result
    }
}
```

変換元 : https://developers.google.com/analytics/devguides/collection/firebase/android/webview#implement_native_interface

必要なimport文は赤字部分にマウスオーバーして <kbd><kbd>Alt</kbd>+<kbd>Enter</kbd></kbd> で適宜追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/981bd185-cda4-20e2-e6cf-4223bef94d30.png)
Javaの場合 https://firebase.google.com/docs/analytics/webview?platform=android&hl=en#implement_native_interface
（iOSアプリの場合はこちら :  https://firebase.google.com/docs/analytics/webview?platform=ios#implement_native_interface ）

#### eCommerceイベントを計測したいとき
現在公式ヘルプにサンプルコードとして記載されているネイティブインターフェースのコードはitemsパラメータに対応していない。もしeCommerceイベントを計測したいときは`bundleFromJson()`関数を以下のように変更する。
```kotlin
private fun bundleFromJson(json: String): Bundle? {
    if (TextUtils.isEmpty(json)) {
        return Bundle()
    }
    val result = Bundle()
    try {
        val jsonObject = JSONObject(json)
        val keys = jsonObject.keys()
        while (keys.hasNext()) {
            val key = keys.next()
            val value = jsonObject[key]
            when(value){
                is String -> result.putString(key, value)
                is Int -> result.putInt(key, value)
                is Double -> result.putDouble(key, value)
                is JSONArray -> if(Regex("^items$").matches(key)){
                    val items = ArrayList<Bundle>()
                    for(i in 0 until value.length()){
                        val itemObj = value.getJSONObject(i)
                        val itemKeys = itemObj.keys()
                        val item = Bundle()
                        while(itemKeys.hasNext()){
                            val itemKey = itemKeys.next()
                            val itemValue = itemObj[itemKey]
                            when(itemValue) {
                                is String -> item.putString(itemKey, itemValue)
                                is Int -> item.putInt(itemKey, itemValue)
                                is Double -> item.putDouble(itemKey, itemValue)
                                else -> Log.w(TAG, "Value for item key $itemKey not one of [String, Integer, Double]")
                            }
                        }
                        items.add(item)
                    }
                    result.putParcelableArrayList(key,items)
                }else{
                    Log.w(TAG, "Value for key $key not one of [JSONArray(item)]")
                }
                else -> Log.w(TAG, "Value for key $key not one of [String, Integer, Double, JSONArray(item)]")
            }
        }
    } catch (e: JSONException) {
        Log.w(TAG, "Failed to parse JSON, returning empty Bundle.", e)
        return Bundle()
    }
    return result
}
```

## ネイティブインターフェースをWebViewに紐づけ
先ほど作成したネイティブインターフェースをWebViewリソースに紐づけ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/e2c46079-d9bc-c55c-78e7-46edef9677d3.png)

### Kotlin コード
コード内の「 `webview` 」の文字を目的のリソースIDに変更の上でご利用ください。

```kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    webview.addJavascriptInterface(AnalyticsWebInterface(this), AnalyticsWebInterface.TAG);
} else {
    Log.w(AnalyticsWebInterface.TAG, "Not adding JavaScriptInterface, API Version: " + Build.VERSION.SDK_INT);
}
```

## （オプション）テストアプリのWebViewのデバッグを許可
もしテストアプリのWebView内での通信内容やJavaScriptの詳細な動作などの情報を[PC側のChrome Deveroper Toolで確認](https://developer.chrome.com/docs/devtools/remote-debugging/)したい場合、以下の記述も追加が必要です。
<font color="Red">※ セキュリティ上の問題を生む場合があるため本番アプリでこの記述は行わないで下さい。</font>
### Kotlin コード
コード内の「 `webview` 」の文字を目的のリソースIDに変更の上でご利用ください。
```kotlin
webView.setWebContentsDebuggingEnabled(true);
```
https://developer.android.com/reference/android/webkit/WebView#setWebContentsDebuggingEnabled(boolean)

# DebugViewによるAndroidアプリのイベント計測状況の確認
## 手順
1. PC にadbコマンドを導入する → [ADBコマンド導入の方法 - Qiita](https://qiita.com/hikaru__m/items/15baae425b6fad25da05)
2. 検証用の Android 端末にデバッグ対象のアプリをインストール
3. Android 端末のデバッグモードを有効化
4. Android 端末をPCにUSBデバッグモードで接続
5. PC でコマンドプロンプトを起動し、コマンド `adb devices` を実行。以下のように端末名と`device`の文字が表示されれば次の手順へ。`unauthorize`と出た場合は端末側でUSBデバッグの認証が通っていないので端末を操作して許可させる。
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/18d69bd8-ce79-dbb8-7e39-f3ccff96613c.png)
  - [adbコマンドが実行できない時の対処法 - Qiita](https://qiita.com/TsubasaSato/items/da821e082048f994202f)
6. コマンド `adb shell setprop debug.firebase.analytics.app ●●●`を実行（黒丸部分はデバッグ対象のパッケージ名に置換えた上で実行）
7. Android 端末を PC から外してアプリを操作する
8. PC のブラウザで Firebase Console を開き、目的の Firebase プロジェクト画面の左側ナビゲーション内の 分析 > DebugView を選択して開く
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/23097a34-0e29-38f9-18eb-6ecbff8fadbc.png)
9. 「デバッグに使用するデバイス」に先ほど自分が設定した Android 端末があることを確認して選択
10. Android 端末で操作した内容が反映されるか確認する（15秒程度の遅延あり）

[イベントのデバッグ  |  Firebase](https://firebase.google.com/docs/analytics/debugview?hl=ja#enabling_debug_mode)

## イベントが計測されている様子
以下のようにイベントが計測されれば実装および検証が成功。お疲れ様でした。

DebugViewで目的のイベントが表示されることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/28156f92-d9bc-a977-3d48-eb97beb5db17.png)

表示されたイベントをクリックしてパラメータも正常に計測できていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/e464a7d3-7f13-804e-b686-f2aa319a01d3.png)

GA4側のDebugViewでも確認可能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/d2db954c-63f9-16fb-291a-22b3825cb06f.png)

<hr>

以上。

#参考
WebView でアナリティクスを使用する  |  Firebase

- Android(Java) : https://developers.google.com/analytics/devguides/collection/firebase/android/webview
- iOS(Swift, Objective-C) : https://firebase.google.com/docs/analytics/webview?platform=iOS