概要:モバイルアプリに組み込まれたWebViewからアプリ内イベントを記録し、アプリ内のWebコンテンツ上で行われた操作も漏れなく計測します。
はじめに
アプリ内で発生するアプリ内イベントは、SDKを使って送信します。アプリのネイティブ環境以外で発生するイベントは、どのように計測すればよいでしょうか?
ネイティブ環境以外でイベントが発生するケースには、次のようなものがあります。
- Webサイト上で発生するイベント:People-Based Attribution(PBA)を実装すると、チャネル、Webサイトなどのプラットフォーム、デバイスを横断して、ユーザーの一連の行動をまとめて把握できます。 Web-to-App、コンバージョン経路の分析、ローデータを利用できます。
- バックエンドサーバー:Webサイトやアプリ上でのユーザー操作とは独立してイベントが発生します。例えば、サブスクリプションの自動更新などです。
- ハイブリッドアプリ:本記事で説明するように、アプリがインストールされているデバイス上のモバイル向けWebサイトでイベントが発生します。 JavaScriptからAppsFlyer SDK APIを呼び出して、アプリ内イベントを記録します。
このガイドでは、ハイブリッドアプリのケースを取り上げます。 HTML画面とネイティブ画面を連携させ、HTML画面のイベントを記録してアプリに送信する方法を説明します。
注意
Cordova、Ionic、React Native、Flutterなどのハイブリッドプラグインを使ってAppsFlyerを実装している場合、このガイドの連携方法は適用できません。
この場合、WebViewで発生するイベントは、AppsFlyerのサーバー間(S2S)APIで送信することを推奨します。
例
サブスクリプションを提供するハイブリッドアプリがあるとします。 Webサイト上のサブスクリプション申し込みフォームを、WebViewで読み込むようにします。
WebViewでサブスクリプションのアプリ内イベントを記録し、種類や価格などの関連データをネイティブコードに送信できます。
ネイティブコードはデータを受け取り、AppsFlyer SDKを使ってサブスクリプションのアプリ内イベントを送信します。
ハイブリッドアプリでのアプリ内イベントの記録
このガイドでは、ハイブリッドアプリでアプリ内イベントを記録する2つの方法を紹介します。
- [推奨]JavaScriptインターフェース:ネイティブのJavaScriptインターフェースを使い、HTML画面やWebViewとネイティブコードの間でデータをやり取りします。 これにより、WebViewからネイティブコードにアプリ内イベントの関連データを送信できます。ネイティブコードがデータを受け取ると、SDKを使ってAppsFlyerに送信します。
- URLの読み込み:ネイティブコードがURLの読み込みイベントを監視する方法です。 特定のURLの読み込みイベントを監視し、URLパラメータからデータを取得するよう、ネイティブコードを設定できます。取得したデータをSDKに渡します。
JavaScriptインターフェース
AndroidとiOSには、WebViewからネイティブコードを呼び出すためのネイティブJavaScriptインターフェースがあります。
- Android:JavaScriptInterface
- iOS:JavaScriptCore
URLの読み込み方式と比較した、ネイティブJavaScriptインターフェースの利点:
- URLの読み込みを監視する処理を実装する必要がありません。
- 他の実装方法よりも、ネイティブインターフェースの利用が推奨されます。
- URLのパラメータを解析する必要がありません。
- コード量が少なく、保守の負担も軽減されます。
実装手順は以下のとおりです。
- WebViewまたはWebページへのHTMLコードの追加
- WebViewのネイティブコードの実装
Android
Android向けのHTMLコード
WebViewまたはWebページに、次のHTMLコードを追加します。
<h1>Recording Event From Web View</h1>
<div id="main">
<button id="recordEvent" onclick="recordEvent()"> Record Event </button>
</div>
<script type="text/JavaScript">
<!-- Set Customer User ID -->
function setCustomerUserId(){
app.setCustomerUserId(customerUserId)
}
function recordEvent(){
var eventName = "af_purchase"
var eventParams = "{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}";
app.recordEvent(eventName, eventParams)
}
</script>
イベントの値を渡す際は、以下に注意してください。
-
イベント値は、文字列化したJSONとして渡してください。
"{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}" - 問題の発生を避けるため、AppsFlyerでは、アプリ内イベント名に小文字の英数字(a~z、0~9)のみを使用することを推奨します。
WebActivityクラス
次のコードを使用して、WebActivityクラスを作成します。
public class WebActivity extends AppCompatActivity {
WebView web;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
web = (WebView) findViewById(R.id.webView);
web.getSettings().setJavaScriptEnabled(true);
web.addJavaScriptInterface(new MainJsInterface(getApplicationContext()), "app");
web.loadUrl("https://yourwebsite.com");
}
}
上記のコードは、WebViewとネイティブコードを橋渡しするappというオブジェクトを作成します。
JavaScriptインターフェースクラス
MainJsInterfaceクラスを作成し、recordEvent()をJavaScriptInterface経由で呼び出せるように実装します。
public class MainJsInterface {
Context mContext;
MainJsInterface(Context c) {
mContext = c;
}
//Set Customer User ID
@JavaScriptInterface
public void setCustomerUserId(String customerUserId){
AppsFlyerLib.getInstance().setCustomerUserId(customerUserId);
}
@JavaScriptInterface
public void recordEvent(String name, String json){
Map<String, Object> params = null;
if(json!=null) {
try {
JSONObject jsonObject = new JSONObject(json);
params = new HashMap<>();
Iterator keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.opt(key);
params.put(key, value);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
AppsFlyerLib.getInstance().logEvent(this.mContext, name, params);
}
}
上記のコードは、WebView内のJavaScriptから呼び出せるrecordEventメソッドを宣言します。
HTMLコードでは、app.recordEvent(eventName, eventParams)でこのメソッドを呼び出しています。前述のとおり、appはWebViewとネイティブコードを橋渡しするよう設定されています。そのため、appからMainJsInterfaceクラスで定義したrecordEventメソッドを呼び出せます。
iOS
iOS向けのHTMLコード
WebViewまたはWebページに、次のHTMLコードを追加します。
<body>
<h1>Recroding Event From Web View</h1>
<div id="main">
<button id="recordEvent" onclick="recordEvent()"> Record Event </button>
</div>
<script type="text/JavaScript">
<!-- Set Customer User ID -->
function setCustomerUserId(){
customerUserIdwebkit.messageHandlers.cuid.postMessage(customerUserId);
}
function recordEvent(){
var eventName = "af_purchase"
var eventParams = "{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}";
webkit.messageHandlers.event.postMessage(eventName + "+" + eventParams);
}
</script>
</body>
上記のコードは、ボタンのクリック時に実行する関数を設定します。
この関数はeventNameとeventParams変数を設定し、webkit.messageHandlersを使ってネイティブコードに渡します。
イベントの値を渡す際は、以下に注意してください。
-
イベント値は、文字列化したJSONとして渡してください。
"{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}" - 問題の発生を避けるため、AppsFlyerでは、アプリ内イベント名に小文字の英数字(a~z、0~9)のみを使用することを推奨します。
注意
このHTMLコードは、Objective-CとSwiftの両方で使用できます。
Objective-C
WebViewコントローラー
WebViewのビューコントローラーに、次のコードを追加します。
-(void)loadView{
[super loadView];
WKWebViewConfiguration *configuration =
[[WKWebViewConfiguration alloc] init];
[configuration.userContentController
addScriptMessageHandler:self name:@"event"];
_webView = [[WKWebView alloc] initWithFrame:self.view.frame
configuration:configuration];
[self.view addSubview:_webView];
}
- (void)viewDidLoad {
[super viewDidLoad];
NSString* page = @"https://yourwebsite.com";
NSURL *url = [NSURL URLWithString:page];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[_webView loadRequest:request];
}
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
NSString* messageBody = message.body;
NSString* eventName = [messageBody componentsSeparatedByString:@"+"][0];
NSString* eventValue = [messageBody componentsSeparatedByString:@"+"][1];
[self recordEvent:eventName eventValue:eventValue];
}
- (void)recordEvent:(NSString*)eventName eventValue:(NSString*)eventValue{
NSData *eventValuedata = [eventValue dataUsingEncoding:(NSUTF8StringEncoding)];
NSDictionary *eventValueDict = [NSJSONSerialization JSONObjectWithData:eventValuedata options:NSJSONReadingMutableContainers error:nil];
[[AppsFlyerLib shared] logEvent:eventName withValues:eventValueDict];
}
上記のコードは、WebViewからメッセージを受け取ります。メッセージには、イベント名とイベント値が含まれます。メッセージを受信すると、recordEventメソッドに渡して値を解析し、ネイティブSDKのlogEventメソッドを呼び出します。
SwiftのWebViewコントローラー
WebViewコントローラーに、次のコードを追加します。
import WebKit
import UIKit
import AppsFlyerLib
import JavaScriptCore
class JSViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "event" {
let messageBody = message.body as? String
let eventName = messageBody?.components(separatedBy: "+")[0]
let eventValue = messageBody?.components(separatedBy: "+")[1]
recordEvent(eventName: eventName!, eventValue: eventValue!)
} else if message.name == "cuid" {
AppsFlyerLib.shared().customerUserID = message.body as? String
}
}
var webView: WKWebView!
override func loadView(){
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://yourwebsite.com")!
webView.load(URLRequest(url: url))
webView.configuration.userContentController.add(self, name:"event")
}
func recordEvent(eventName: String, eventValue: String) {
var eventValueDict = [String: String]()
let eventValuesData = eventValue.data(using: String.Encoding.utf8)
do {
eventValueDict = try (JSONSerialization.jsonObject(with: eventValuesData!, options:[]) as? [String: String])!
} catch let error as NSError{
print(error)
}
AppsFlyerLib.shared().logEvent(eventName as String?, withValues: eventValueDict)
}
}
上記のコードは、WebViewからメッセージを受け取ります。メッセージには、イベント名とイベント値が含まれます。メッセージを受信すると、recordEventメソッドに渡して値を解析し、AppsFlyer SDKのlogEventメソッドを呼び出します。
URLの読み込み
AndroidとiOSには、URLの読み込みイベントを監視するネイティブメソッドがあります。この方法では、次のことが可能です。
- 特定のURLの読み込みイベントの監視
- URLに追加されたパラメータの抽出
- ネイティブコードでのパラメータの利用
以下の実装は、2つの手順で構成されます。
- WebViewまたはWebページへのHTMLコードの追加
- URL読み込み処理のネイティブコードの実装
WebViewの実装
HTMLコード(Android WebViewとiOS WKWebViewの両方に対応)
<html>
<head>
</head>
<body>
<h1>Recording From Web View</h1>
<div id="main">
<input id="button" type="button" value="Record Event" />
</div>
<script type="text/JavaScript">
function recordEvent(eventName,eventValue){
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "af-event://inappevent?eventName="+eventName+"&eventValue="+eventValue);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
var button = document.getElementById("button");
button.onclick = function(event) {
var eventName = "af_purchase";
var eventValue = "{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}";
recordEvent(eventName, eventValue);
}
</script>
</body>
</html>上記のコードは、実際のWebページに組み込むことも、ネイティブ側でWebViewに読み込むこともできます。
このコードは、ボタンのクリック時に実行する関数を設定します。関数は、独自のURLを読み込むiframe要素を作成します。このURLの読み込みをきっかけに、AndroidまたはiOSのネイティブメソッドが実行され、AppsFlyer SDKのlogEventメソッドが呼び出されます。
必要なすべてのパラメータ(イベント名とイベント値)は、URLに追加されます。以下のAndroidとiOSのコード例では、これらのパラメータを取得し、AppsFlyer SDKのlogEventメソッドに渡す方法を示しています。
イベントの値を渡す際は、以下に注意してください。
-
イベント値は、文字列化したJSONとして渡してください。
"{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}" - 問題の発生を避けるため、AppsFlyerでは、アプリ内イベント名に小文字の英数字(a~z、0~9)のみを使用することを推奨します。
ネイティブ側の実装
Androidでの実装
Android WebViewは、WebViewからの通知を受け取るWebViewClientを使用します。 shouldOverrideUrlLoadingで、WebViewからのURL読み込みイベントを処理できます。
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("af-event://")) {
String[] urlParts = url.split("\\?");
if (urlParts.length > 1) {
String query = urlParts[1];
String eventName = null;
HashMap<String, Object> eventValue = new HashMap<>();
for (String param : query.split("&")) {
String[] pair = param.split("=");
String key = pair[0];
if (pair.length > 1) {
if ("eventName".equals(key)){
eventName = pair[1];
} else if ("eventValue".equals(key)){
JSONObject event;
JSONArray keys;
try {
event = new JSONObject(pair[1]);
keys = event.names();
for (int i = 0; i < keys.length(); i++){
eventValue.put(keys.getString(i), event.getString(keys.getString(i)));
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
AppsFlyerLib.getInstance().logEvent(getApplicationContext(),eventName,eventValue);
}
return true;
}
view.loadUrl(url);
return true;
}
iOSでの実装
WebViewのビューコントローラーのshouldStartLoadWithRequestメソッドに、次のコードを追加します。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *page = @"https://yourwebsite.com";
NSURL *url = [NSURL URLWithString:page];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
_webView = [[WKWebView alloc] initWithFrame:self.view.frame];
_webView.navigationDelegate = self;
[_webView loadRequest:request];
_webView.frame = CGRectMake(self.view.frame.origin.x,self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
[self.view addSubview:_webView];
}
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction* )navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
decisionHandler(WKNavigationActionPolicyAllow);
NSString *url = navigationAction.request.URL.absoluteString;
if([url containsString:(@"af-event")]){
[self recordEvent:(url)];
}
}
- (void)recordEvent:(NSString*)url{
NSString *eventNameAndEventValueString = [url componentsSeparatedByString: @"?"][1];
NSArray *eventNameAndEventValueArray = [eventNameAndEventValueString componentsSeparatedByString: @"&"];
NSString *eventName = [eventNameAndEventValueArray[0] componentsSeparatedByString: @"="][1];
NSString *eventValueParams = [eventNameAndEventValueArray[1] componentsSeparatedByString: @"="][1];
NSString *decodedValueParams = [eventValueParams stringByRemovingPercentEncoding];
NSData *eventValuedata = [decodedValueParams dataUsingEncoding:(NSUTF8StringEncoding)];
NSDictionary *eventValue = [NSJSONSerialization JSONObjectWithData:eventValuedata options:NSJSONReadingMutableContainers error:nil];
if (eventName != nil){
[[AppsFlyerLib shared] logEvent:eventName withValues:eventValue];
}
}
上記のコードは、すべてのURLの読み込みを監視します。 「af-event」を含むURLが読み込まれると、ネイティブのrecordEventメソッドが実行されます。ネイティブのrecordEventメソッドが必要なパラメータ(eventNameとeventValue)を取得し、ネイティブSDKのlogEventメソッドに渡します。
WebViewのビューコントローラーのwebView(_:decidePolicyFor:decisionHandler:)メソッドに、次のコードを追加します。
import WebKit
import UIKit
import AppsFlyerLib
class WebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView(){
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://yourwebsite.com")!
webView.load(URLRequest(url: url))
}
func webView(_ view: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
decisionHandler(.allow)
let pageUrl = navigationAction.request.url?.absoluteString
if(pageUrl?.hasPrefix("af-event://") ?? false){
let scanner = Scanner(string: pageUrl!)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "&?")
scanner.scanUpTo("?", into: nil)
var tempString, eventName : NSString?
var eventValue: [String: String] = Dictionary()
while(scanner.scanUpTo("&", into: &tempString)){
if let tempString = tempString {
if(tempString.hasPrefix("eventName=") && tempString.length > 10){
eventName = tempString.substring(from: 10) as NSString
}
if(tempString.hasPrefix("eventValue=") && tempString.length > 11){
let eventValues = tempString.components(separatedBy: "=")[1].removingPercentEncoding
let eventValuesData = eventValues?.data(using: String.Encoding.utf8)
do {
eventValue = try (JSONSerialization.jsonObject(with: eventValuesData!, options:[]) as? [String: String])!
} catch let error as NSError{
print(error)
}
}
}
}
if(eventName != nil){
if let eventName = eventName {
// record event
AppsFlyerLib.shared().logEvent(eventName as String?, withValues: eventValue)
}
}
}
}
}
上記のコードは、すべてのURLの読み込みを監視します。 「af-event」を含むURLが読み込まれると、必要なパラメータ(eventNameとeventValue)を取得し、ネイティブSDKのlogEventメソッドに渡します。