본문 바로가기

English/Some Dev Stuff

[Firebase Analytics] Setting up setUserId in a WebView

This guide is designed for rare situations where setting the userId directly in the native app is impractical. If such a case arises, providing this guide to the client might prove helpful.

 

  1. Implementing Javascript Handler
    // [START log_event]
    function logEvent(name, params) {
      if (!name) {
        return;
      }
    
      if (window.AnalyticsWebInterface) {
        // Call Android interface
        window.AnalyticsWebInterface.logEvent(name, JSON.stringify(params));
      } else if (window.webkit
          && window.webkit.messageHandlers
          && window.webkit.messageHandlers.firebase) {
        // Call iOS interface
        var message = {
          command: 'logEvent',
          name: name,
          parameters: params
        };
        window.webkit.messageHandlers.firebase.postMessage(message);
      } else {
        // No Android or iOS interface found
        console.log("No native APIs found.");
      }
    }
    // [END log_event]
    
    // [START set_user_property]
    function setUserProperty(name, value) {
      if (!name || !value) {
        return;
      }
    
      if (window.AnalyticsWebInterface) {
        // Call Android interface
        window.AnalyticsWebInterface.setUserProperty(name, value);
      } else if (window.webkit
          && window.webkit.messageHandlers
          && window.webkit.messageHandlers.firebase) {
        // Call iOS interface
        var message = {
          command: 'setUserProperty',
          name: name,
          value: value
       };
        window.webkit.messageHandlers.firebase.postMessage(message);
      } else {
        // No Android or iOS interface found
        console.log("No native APIs found.");
      }
    }
    // [END set_user_property]
    
    // [START set_user_id]
    function setUserId(id) {
      if (!id) {
        return;
      }
    
      if (window.AnalyticsWebInterface) {
        // Call Android interface
        window.AnalyticsWebInterface.setUserId(id);
      } else if (window.webkit
          && window.webkit.messageHandlers
          && window.webkit.messageHandlers.firebase) {
        // Call iOS interface
        var message = {
          command: 'setUserId',
          id: id
        };
        window.webkit.messageHandlers.firebase.postMessage(message);
      } else {
        // No Android or iOS interface found
        console.log("No native APIs found.");
      }
    }
    // [END set_user_id]
    
    document.getElementById("event1").addEventListener("click", function() {
        console.log("event1");
        logEvent("event1", { foo: "bar", baz: 123 });
    });
    
    document.getElementById("event2").addEventListener("click", function() {
      console.log("event2");
        logEvent("event2", { size: 123.456 });
    });
    
    document.getElementById("userprop").addEventListener("click", function() {
        console.log("userprop");
        setUserProperty("userprop", "custom_value");
    });
  2. Below is an extension of Google's basic WebView guide, specifically adding the [setUserId](<https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics#setUserId(java.lang.String)>) function. Through this, the id value is transmitted to the WebView via JavaScript, similar to how logEvent and setUserProperty work.
  3. Implementing Native Interface
    1. Android - Simply add the setUserId section
      package com.google.firebase.quickstart.analytics.webview;
      
      import android.content.Context;
      import android.os.Bundle;
      import android.text.TextUtils;
      import android.util.Log;
      import android.webkit.JavascriptInterface;
      
      import com.google.firebase.analytics.FirebaseAnalytics;
      
      import org.json.JSONException;
      import org.json.JSONObject;
      
      import java.util.Iterator;
      
      // [START analytics_web_interface]
      public class AnalyticsWebInterface {
      
          public static final String TAG = "AnalyticsWebInterface";
          private FirebaseAnalytics mAnalytics;
      
          public AnalyticsWebInterface(Context context) {
              mAnalytics = FirebaseAnalytics.getInstance(context);
          }
      
          @JavascriptInterface
          public void logEvent(String name, String jsonParams) {
              LOGD("logEvent:" + name);
              mAnalytics.logEvent(name, bundleFromJson(jsonParams));
          }
      
          @JavascriptInterface
          public void setUserProperty(String name, String value) {
              LOGD("setUserProperty:" + name);
              mAnalytics.setUserProperty(name, value);
          }
      
          @JavascriptInterface
          public void setUserId(String id) {
              LOGD("setUserId:" + id);
              mAnalytics.setUserId(id);
          }
      
          private void LOGD(String message) {
              // Only log on debug builds, for privacy
              if (BuildConfig.DEBUG) {
                  Log.d(TAG, message);
              }
          }
      
          private Bundle bundleFromJson(String json) {
              // [START_EXCLUDE]
              if (TextUtils.isEmpty(json)) {
                  return new Bundle();
              }
      
              Bundle result = new Bundle();
              try {
                  JSONObject jsonObject = new JSONObject(json);
                  Iterator<String> keys = jsonObject.keys();
      
                  while (keys.hasNext()) {
                      String key = keys.next();
                      Object value = jsonObject.get(key);
      
                      if (value instanceof String) {
                          result.putString(key, (String) value);
                      } else if (value instanceof Integer) {
                          result.putInt(key, (Integer) value);
                      } else if (value instanceof Double) {
                          result.putDouble(key, (Double) value);
                      } else {
                          Log.w(TAG, "Value for key " + key + " not one of [String, Integer, Double]");
                      }
                  }
              } catch (JSONException e) {
                  Log.w(TAG, "Failed to parse JSON, returning empty Bundle.", e);
                  return new Bundle();
              }
      
              return result;
              // [END_EXCLUDE]
          }
      
      }
      // [END analytics_web_interface]
    2. iOS
      1. To rectify this, the guard statement should be refined to account for both setUserProperty and logEvent functionalities, rather than being limited solely to 'name'. It seems necessary to extend the guard condition to consider the presence or absence of these properties, ensuring a smooth execution of the code within the guard block.
      2. In the iOS implementation, a simple addition proved insufficient due to a specific issue arising when 'name' was absent, causing a disruption in the process. This commonly happens in scenarios where 'name' is not available, resulting in a failure of the guard statement and, subsequently, halting the execution of the code within the guard block.
import WebKit

import FirebaseAnalytics

class ViewController: UIViewController, WKScriptMessageHandler {

  private var webView: WKWebView!
  private var projectURL: URL!

  override func viewDidLoad() {
    super.viewDidLoad()

    // Get the hosted site url from the GoogleService-Info.plist file.
    let plistPath = Bundle.main.path(forResource: "GoogleService-Info",
                                     ofType: "plist")!
    let plist = NSDictionary(contentsOfFile: plistPath)!
    let appID = plist["PROJECT_ID"] as! String

    let projectURLString = "https://\(appID).firebaseapp.com"
    self.projectURL = URL(string: projectURLString)!

    // Initialize the webview and add self as a script message handler.
    self.webView = WKWebView(frame: self.view.frame)

    // [START add_handler]
    self.webView.configuration.userContentController.add(self, name: "firebase")
    // [END add_handler]

    self.view.addSubview(self.webView)
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let request = URLRequest(url: self.projectURL)
    self.webView.load(request)
  }


  // [START handle_messages]
  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,    
          let value = body["value"] as? [String: NSObject] else { return }
    Analytics.setUserProperty(value, forName: name)
  } else if command == "logEvent" {
    guard let name = body["name"] as? String,
          let params = body["parameters"] as? [String: NSObject] else { return }
    Analytics.logEvent(name, parameters: params)
  } else if command == "setUserId" {
    guard let id = body["id"] as? [String: NSObject] else { return } 
    Analytics.setUserId(id)
  }
}
// [END handle_messages]