概要:记录发生在移动应用内嵌网页视图(web-views)中的应用内事件,即使用户交互发生在应用的网页内容中,也能实现完整的事件衡量。
简介
应用内事件(即在相关应用内发生的事件)通过SDK上报,那么应用之外发生的事件要如何上报呢?
在下列场景中,用户会在相关应用之外的环境中完成事件:
- 用户在您的网站中完成事件:使用基于用户的归因(PBA)方案,对于跨流量入口和操作平台(包括网站和设备)的用户链路实现全面覆盖,获取经过标准化的综合数据。该方案提供网页到应用归因、转化路径分析以及相关的原始数据。
- 后端服务器:事件的发生与网页或应用中的用户行为无关,比如自动续订事件。
- 混合模式: 事件发生在移动端网页,但用户设备中已安装相关应用(具体示例请见下文说明)。 通过Javascript调用AppsFlyer SDK API,实现应用内事件的记录。
本文说明适用于混合模式下的使用场景,解释了如何记录HTML视图中的事件,并将其发送到相关应用,从而打通HTML和原生视图之间的数据传输。
示例
贵司开发了一个混合模式的应用,并向用户提供订阅套餐。您在网页视图中添加了订阅表格,该视图通过您的网站加载。
您可以在网页视图中记录订阅事件,并向原生代码发送套餐类型或价格等相关数据。
原生代码收集这些数据,并使用AppsFlyer SDK发送这些订阅事件。
在混合模式下记录应用内事件
本文针对混合模式下的应用内事件,介绍了以下两种事件记录方式:
- 【推荐】JavaScript接口:使用原生JavaScript接口,打通HTML/网页视图(Web View)和原生代码之间的数据传输, 以便将应用内事件的相关数据从网页视图发送到原生代码。原生代码收到这些数据后,即通过SDK将其发送到AppsFlyer。
- URL加载:您可以让原生代码监听特定URL的加载事件, 并从URL参数重提取数据,然后将这些数据传递给SDK。
JavaScript接口
安卓和iOS都有原生的JavaScript接口,网页视图可以通过这些接口调用原生代码。
与URL加载相比,JavaScript接口具有以下优势:
- 不需要添加用于监听URL加载的逻辑。
- 原生接口优于额外配置。
- 不需要解析URL中的参数。
- 代码更少,减轻维护负担。
部署流程包含以下两个步骤:
- 将HTML代码添加到网页视图或网页
- 配置用于网页视图的原生代码
安卓
适用于安卓的HTML代码
请将以下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"); } }
上述代码可创建一个名为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); } }
上述代码声明了一个可在网页视图中通过Javascript调用的recordEvent
方法。
请注意,在上文的HTML代码中,该方法通过app.recordEvent(eventName, eventParams)
调用。
这时我们已经设置了连接网页视图与原生代码app.recordEvent(eventName, eventParams)
appapp.recordEvent(eventName,>,
因此app可以调用我们在MainJsInterface类中定义的recordEvent
方法。
iOS
适用于iOS的HTML代码
请将以下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
网页视图控制器
在您的网页视图控制器(web view controller)中添加以下代码:
-(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]; }
上述代码会接收网页视图发出的消息,该消息中包含事件名称和事件值。当该代码检测到有消息传入时,会将消息传递给recordEvent
方法,然后该方法会解析这些事件值,并调用原生SDK的logEvent
方法。
适用于Swift的WebView控制器
在您的网页视图控制器(web view controller)中添加以下代码:
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) } }
上述代码会接收网页视图发出的消息,该消息中包含事件名称和事件值。
当该代码检测到有消息传入时,会将消息传递给recordEvent
方法,然后该方法会解析这些事件值,并调用AppsFlyer SDK的logEvent
方法。
URL加载
安卓和iOS都部署了可监听URL加载事件的原生方法。您可以使用该方法完成以下操作:
- 监听特定URL的加载事件
- 提取附加在URL末尾的参数
- 在原生代码中使用这些参数
部署流程包含以下两个步骤:
- 将HTML代码添加到网页视图或网页
- 配置用于URL加载的原生代码
WebView部署方式
HTML代码(适用于安卓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>
您可以将上述代码作为实际网页的一部分,或者以原生方式将其载入网页视图。
该代码可设置一个函数,用户点击按钮时即会触发该函数。该函数本身可创建一个用于加载自定义URL的iframe元素。此处的URL加载会触发安卓或iOS的原生方法,继而调用AppsFlyer SDK的logEvent
方法。
所有必要参数(即事件名称和事件值)都会被添加到该URL的末尾。在下文的安卓和iOS代码示例中,我们会提取这些参数,并将其传递到AppsFlyer SDK的logEvent
方法。
传递事件值时,须注意以下几点:
- 确保事件值为JSON格式的字符串。
"{\"af_revenue\":\"6.72\", \"af_content_type\": \"wallets\", \"af_content_id\": \"15854\"}"
- 建议仅使用小写字母和数字(a-z 和0-9)来命名您的应用内事件,以避免在传递数据时出现任何问题。
原生部署
安卓部署方式
安卓WebView使用WebViewClient接收网页视图发出的通知。您可以通过shouldOverrideUrlLoading
处理网页视图产生的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部署方式
在网页视图控制器(web view controller)的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
方法。
在网页视图控制器(web view controller)的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
方法。