【GA4】アプリストリームにウェブと共通のユーザーIDをWebViewを通して設定する

GA4では[アプリとウェブで共通のユーザーIDを利用する](https://support.google.com/analytics/answer/9213390
)ことでプラットフォームを跨いで同一ユーザーを識別することが可能ですが、実際のケースでは以下のような理由で実現が困難なケースも多いです。

- アプリ側にログイン機能がない
- アプリとウェブで使用しているIDが別種

本記事ではウェブ側のログインID情報をWebViewを通して取り込み、アプリ側の計測でも使えるようにする方法を解説します。


https://support.google.com/analytics/answer/9213390?hl=ja&ref_topic=9303474

# 手順
概ね以下の記事と同じ流れでの実装となりますが、Googleが提供しているJavaScriptハンドラとネイティブインターフェイスのコードに少し記述を書き足して**WebViewを通してアプリストリームにUserIdを送信する機能**を追加します。

https://qiita.com/aqril_1132/items/5997fc611ee227e7a567

https://qiita.com/aqril_1132/items/96fe2dabd04fbf43e25b

## JavaScriptハンドラへの機能追加
Googleが提供している[JavaScriptハンドラのサンプルコード](https://firebase.google.com/docs/analytics/webview?platform=android#implement-javascript-handler)の末尾に以下のコードを書き加えます。

```js
function setUserId(value) {
    if (!value) {
      return;
    }
    if (window.AnalyticsWebInterface) {
      // Call Android interface
      window.AnalyticsWebInterface.setUserId(value);
    } else if (window.webkit
        && window.webkit.messageHandlers
        && window.webkit.messageHandlers.firebase) {
      // Call iOS interface
      var message = {
        command: 'setUserId',
        value: value
     };
      window.webkit.messageHandlers.firebase.postMessage(message);
    } else {
      // No Android or iOS interface found
      console.log("No native APIs found.");
    }
  }
```

## ネイティブインターフェイスへの機能追加

Googleが提供している各言語の[ネイティブインターフェイスのコード](https://firebase.google.com/docs/analytics/webview?platform=android#implement_native_interface)にsetUserId機能を追加します。

### 参考
https://firebase.google.com/docs/analytics/userid?hl=ja

### サンプルコード
#### Swift

```swift
func userContentController(_ userContentController: WKUserContentController,
                         didReceive message: WKScriptMessage) {
  guard let body = message.body as? [String: Any] else { return }
  guard let command = body["command"] as? String else { return }
  

  if command == "setUserProperty" {
    guard let name = body["name"] as? String else { return }
    guard let value = body["value"] as? String else { return }
    Analytics.setUserProperty(value, forName: name)
  } else if command == "logEvent" {
    guard let name = body["name"] as? String else { return }
    guard let params = body["parameters"] as? [String: NSObject] else { return }
    Analytics.logEvent(name, parameters: params)
  } else if command == "setUserId" {
    guard let value = body["value"] as? String else { return }
    Analytics.setUserId(value)
  }
}
```

変更元: https://firebase.google.com/docs/analytics/webview?platform=ios#swift

#### 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)
    }

    @JavascriptInterface
    fun setUserId(value: String) {
        LOGD("setUserProperty:$value")
        firebaseAnalytics.setUserId(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? {
        // [START_EXCLUDE]
        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
        // [END_EXCLUDE]
    }
}
```
変更元 : https://qiita.com/aqril_1132/items/5997fc611ee227e7a567#4-%E3%82%A2%E3%83%97%E3%83%AA%E5%81%B4%E3%81%AB%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96-%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%82%92%E5%AE%9F%E8%A3%85


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/220a7a9c-95b4-a503-4b89-4cd77f45be79.png)


## GTMからユーザーIDをセットする命令を実行できるように設定
以下のタグ設定によってGA4のアプリストリームとウェブストリームに共通のUserIdを送信できるようにします。

#### アプリストリーム向けsetUserIdの設定
以下コードを使ってカスタムHTMLタグを作成。ユーザーIDが取得可能になったタイミングで発火するよう設定します。
`{{dataLayer - userId}}`部分をユーザーIDを取得できる変数に置き換えてご利用ください。
また、必要に合わせて「ログアウト中はタグを発火させない」などの設定を行ってください。


```html:アプリストリーム向けUserIdセット用カスタムHTMLタグ
<script>
  if(typeof setUserId !== 'undefined'){
    setUserId({{dataLayer - userId}});
  }
</script>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/1100558d-b302-ea51-33e1-9ab2d9fa2c6c.png)


#### ウェブストリーム向けsetUserIdタグ
以下のマニュアルを参考にユーザーIDがウェブストリームに送信されるように設定します。

https://support.google.com/tagmanager/answer/4565987

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103957/801b6e72-7d7f-5063-03d9-cd0736b6a09e.png)

以上でGA4のアプリストリームとウェブストリームに共通のUserIdを送信できるようになりました。

# GA4のユーザーID機能を使う上での注意事項

### ユーザーID機能と関連性の高い利用規約の抜粋
記事の最後に、本機能の利用と関連性の高いGoogleの利用規約を抜粋してご紹介いたします。
「メールアドレスの値をユーザーIDとしてGoogleに送信してしまう」など、これらの規約に抵触する実装を行ってしまうと**Googleアナリティクスアカウントが停止**されてすべてのデータが失われるおそれがあります。
Googleの利用規約には変更の可能性があるため、抜粋した内容と最新の内容に差異が発生する場合があります。実装検討の際には必ずGoogleのページにて最新の規約をご確認ください。

また、Googleの利用規約に完全に合致する利用を行った場合でも送信する情報の内容と情報取得までの合意プロセスなどによってはGDPR、CCPA、改正個人情報保護法などプライバシー保護関連法に抵触する可能性があるため、実装前に法務に確認することをお勧めいたします。

> Firebase > ドキュメント > エンゲージメント
>
> **ユーザー ID を設定する**
> 必ずユーザー ID の使用が Google アナリティクス利用規約を遵守していることを確認してください。これには、個人情報を許容できない方法で使用してはならないことと、プライバシー ポリシーにおいて ID の使用法を適切に通知することが含まれます。ユーザー ID には、第三者が使用すると個別ユーザーの身元を特定できる情報が含まれないようにしてください。たとえば、ユーザーのメールアドレスや社会保障番号をユーザー ID として使用することはできません。

https://firebase.google.com/docs/analytics/userid?hl=ja

> Google アナリティクス > 測定 > GA4 > サポート
>
> **Google アナリティクス 4 SDK、User-ID に関するポリシー**
> 
> - お客様は、使用する Google アナリティクスの設定と機能について、エンドユーザーに適切な通知（Google アナリティクスを介してどのようなデータを収集するかについての通知や、すでに保有しているエンドユーザーに関する他のデータに該当のデータが関連付けられる可能性があるかについての通知など）を行う必要があります。お客様は、使用する設定と機能について、エンドユーザーからの同意を得るか、無効にできるようにする必要があります。
> - お客様は、Google が個人を特定できるデータ（氏名、社会保障番号、メールアドレス、または他の同様のデータ）や、特定のデバイスを恒久的に識別できるデータ（リセットできないデバイス固有の ID など）をアップロードすることはできません。
> - お客様は、エンドユーザーによる同意が得られていて、適用される法律や規制で認められている場合を除き、エンドユーザーの認証済みのセッションと未認証のセッションを統合することはできません。
> 
> このポリシーに違反すると、Google アナリティクス アカウントが停止され、Google アナリティクスのデータが失われる場合があります。

https://developers.google.com/analytics/devguides/collection/ga4/policy

> アナリティクス ヘルプ
>
> **個人情報についての Google の考え方**
> Google は、個人情報を、それ単体で個人の特定、個人への接触、個人の所在地の特定を可能にするものと解釈しています。これには次のものが含まれます。
> 
> - メールアドレス
> - 住所
> - 電話番号
> - 高精度な位置情報（GPS 座標など。以下の注を参照）
> - フルネームやユーザー名

https://support.google.com/analytics/answer/7686480

> アナリティクス ヘルプ
>
> 注: ユーザー スコープのカスタム ディメンションを設定する際には、名前、社会保障番号、メールアドレスといった個人情報を（ハッシュ形式であっても）含めないようにしてください。

https://support.google.com/analytics/answer/10075209?hl=ja

> アナリティクス ヘルプ
>
> **ソルト付きでハッシュ化された個人情報**
>**適切な暗号化レベルを使用している場合に限り**、個人情報を含む ID やカスタム ディメンションは、暗号化したうえで Google アナリティクスに送信することができます。Google は、**SHA256 によるハッシュ化を最小要件とし、8 文字以上のソルトを使用**することを強くおすすめしています。ただし、ハッシュやソルトを使用していても、HIPAA で定義されている「保護されるべき医療情報」を暗号化して送信することはできません。

https://support.google.com/analytics/answer/6366371?hl=ja

> Google アナリティクス > Implementation Guides and Solutions
>
> **What constitutes an acceptable User Id**
> If you plan on using your own user identifier to join your offline data with Google Analytics's data, there are a few things to keep in mind when choosing what value to use as your user Id.
> First, per [Google Analytics terms of service](https://marketingplatform.google.com/about/analytics/terms/us/), **you may not use an identifier that contains personally identifiable information (PII).** This rules out email addresses, user logins, social security numbers, phone numbers or any piece of data that is deemed to be "PII".
> You can use non obfuscated alphanumeric database identifiers that you might create for your visitors. Another acceptable option is to pass to Google Analytics an encrypted identifier that is based on PII that is not Protected Health Information (as defined under HIPAA), **as long as you use the proper encryption level**. **Google has a minimum hashing requirement of SHA256 and strongly recommends the use of a salt, minimum 8 characters.**

https://developers.google.com/analytics/solutions/crm-integration#user_id