前言

​ 所谓的 APP 和 H5 打通,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务器,而是先发给 APP 端的数据采集 SDK,经过 APP 端数据采集 SDK 二次加工处理后存入本地缓存再进行同步。

一、App 与 H5 打通原因

1.1 数据丢失率

​ APP 端采集数据的丢失率一般在 1% 左右,而 H5 采集数据的丢失率一般在 5% 左右(主要是因为缓存,网络或切换界面等原因)。因此,如果 APP 与 H5 打通,H5所有事件都可以先发给 APP 端数据采集 SDK,经过 APP 端二次加工处理后并入本地数据库,在符合特定策略后进行数据同步,即可把数据丢失率由 5% 降低到 1% 左右。

1.2 数据准确性

​ 众所周知,H5 无法直接获取设备的相关信息,只能通过解析 UserAgeng 获取有限的信息,而解析 UserAgent 值,至少会面临下面的问题。

(1)有些信息通过解析 UserAgent 值根本获取不到,比如应用程序的版本号等。

(2)有些信息通过解析 UserAgent 值可以获取到,但是内容可能不正确。

​ 如果 APP 和 H5 打通,由 APP 端数据采集 SDK 补充这些信息,即可确保事件信息的准确性和完整性。

1.3 用户标识

​ 对于用户在 APP 端注册或者登录之前,我们一般都是使用用户匿名 ID 来标识用户。而 APP 和 H5 标识匿名用户的规则不一样。进而导致一个用户出现两个匿名 ID 的情况。如果 APP 和 H5 打通,就可以将两个匿名 ID 做归一化处理。

​ APP 和 H5 打通的方案有一下两种。

  • 通过拦截 WebView 请求进行打通。
  • 通过 JavaScript 与 WebView 相互调用进行打通。

二、方案一:拦截请求

​ 拦截 WebView 发送的 URL 请求,即如果是协定好的特定格式,可进行拦截并获取事件数据。如果不是,让请求继续加载。此时 JavaScript SDK 就需要知道,当前 H5 是在 APP 端显示环视在 Safari 浏览器显示,只有在 APP 端显示时,H5 触发事件后,JavaScript SDK 才能向 APP 发送特定的 URL 请求进行打通;如果是在 Safari 浏览器显示,JavaScript SDK 也发送请求进行打通,会导致事件丢失。对于 iOS 应用程序来说,目前常用的方案是借助 UserAgent 来进行判断,即当 H5 在 APP 端显示时,我们可以通过在当前的 UserAgent 上追加一个特殊的标记,进而告知 JavaScript SDK 当前 H5 是在 APP 端显示并需要进行打通。

2.1 修改 UserAgent

​ 我们可以通过下面的方法来修改 UserAgent

- (void)userAgent {
// 创建一个空的 WKWebView
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero];
// 创建一个 self 的弱引用,防止循环引用
__weak typeof (self) weakSelf = self;
// 执行 JavaScript 代码,获取 WKWebView 中的 UserAgent
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 创建强引用
__strong typeof (weakSelf) strongSelf = weakSelf;
// 执行结果 result 为获取到的 UserAgent 值
NSString *userAgent = result;
// 给 UserAgent 追加自己需要的内容
userAgent = [userAgent stringByAppendingString:@" /sa-sdk-ios "];
// 将 UserAgent 字典内容注册到 NSUserDefault 中
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": userAgent}];
// 释放 webView
strongSelf.webView = nil;
}];
}

第一步:新增 SensorsAnalyticsSDK 的类别 WebView ,并新增 - addWebViewUserAgent: 方法声明

NS_ASSUME_NONNULL_BEGIN

@interface SensorsAnalyticsSDK (WebView)

/// 在 WebView 控件中添加自定义的 UserAgent,用于实现打通方案
/// @param userAgent 自定义的 UserAgent
- (void)addWebViewUserAgent:(nullable NSString *)userAgent; @end NS_ASSUME_NONNULL_END

第二步:实现 - addWebViewUserAgent: 方法,并修改 UserAgent 值

#import "SensorsAnalyticsSDK+WebView.h"

#import <WebKit/WebKit.h>

@interface SensorsAnalyticsSDK (WebView)

@property(nonatomic, strong) WKWebView *webView;

@end

@implementation SensorsAnalyticsSDK (WebView)

- (void)loadUserAgent:(void(^) (NSString *))completion {
// 创建一个空的 WKWebView
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero];
// 创建一个 self 的弱引用,防止循环引用
__weak typeof (self) weakSelf = self;
// 执行 JavaScript 代码,获取 WKWebView 中的 UserAgent
[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 创建强引用
__strong typeof (weakSelf) strongSelf = weakSelf;
// 调用回调
completion(result);
// 释放 webView
strongSelf.webView = nil;
}];
} - (void)addWebViewUserAgent:(nullable NSString *)userAgent {
[self loadUserAgent:^(NSString *oldUserAgent) {
// 给 UserAgent 添加自己的内容
NSString *newUserAgent = [oldUserAgent stringByAppendingString:userAgent ?: @" /sa-sdk-ios "];
// 将 UserAgent 字典内容注册到 NSUserDefault 中
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": newUserAgent}];
}];
} @end

​ 在上面的代码中,我们实现了一个加载获取 UserAgent 值得私有方法 - loadUserAgent: 方法,该方法通过回调将 UserAgent 值返回。在 - addWebViewUserAgent: 方法中调用 - loadUserAgent: 方法获取到 UserAgent 旧值,然后追加 /sa-sdk-ios 特殊符号,最后把生成的新的 UserAgent 值注册到 NSUserDefaults 中。

2.2 是否拦截

第一步:声明 - shouldTrackWithWebView: request: 方法。

/// 判断是否需要拦截并处理 JavaScript SDK 发送过来的事件数据
/// @param webView 用于界面展示的 WebView 控件
/// @param request 控件中的请求
- (BOOL)shouldTrackWithWebView:(id)webView request:(NSURLRequest *)request;

第二步:实现 - shouldTrackWithWebView: request: 方法。

- (BOOL)shouldTrackWithWebView:(id)webView request:(NSURLRequest *)request {
// 获取请求的完整路径
NSString *urlString = request.URL.absoluteURL;
// 查找完整路径中是否包含 sensorsanalytics://trackEvent ,如果不包含,则是普通请求,不做处理,返回 NO
if ([urlString rangeOfString:SensorsAnalyticsJavaScriptTrackEventScheme].location == NSNotFound) {
return NO;
} NSMutableDictionary *queryItems = [NSMutableDictionary dictionary];
// 请求中的所有 Query,并解析获取数据
NSArray<NSString *> *allQuery = [request.URL.query componentsSeparatedByString:@"&"];
for (NSString *query in allQuery) {
NSArray<NSString *> *items = [query componentsSeparatedByString:@"="];
if (items.count >= 2) {
queryItems[items.firstObject] = [items.lastObject stringByRemovingPercentEncoding];
}
}
// TODO: 采集请求中的数据
return YES;
}

2.3 二次加工 H5 事件

第一步:新增 - trackFromH5WithEvent: 方法,用于对数据进行加工

- (void)trackFromH5WithEvent:(NSString *)jsonString {
NSError *error = nil;
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *event = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
if (error || !event) {
return;
} NSMutableDictionary *properties = [event[@"properties"] mutableCopy];
// [properties addEntriesFromDictionary:self.automaticProperties];
event[@"_hybrid_h5"] = @(YES); // event[@"distinct_id"] = self.loginId ?: self.anonymousId; // dispatch_async(self.serialQueue, ^{
// // 打印
// [self printEvent:event];
// // [self.fileStroe saveEvent:event];
// [self.database insertEvent:event];
// });
//
// if (self.database.eventCount >= self.flushBulkSize) {
// [self flush];
// }
}

第二步:修改 - shouldTrackWithWebView: request: 方法,添加 - trackFromH5WithEvent: 方法调用

- (BOOL)shouldTrackWithWebView:(id)webView request:(NSURLRequest *)request {
// 获取请求的完整路径
NSString *urlString = request.URL.absoluteURL;
// 查找完整路径中是否包含 sensorsanalytics://trackEvent ,如果不包含,则是普通请求,不做处理,返回 NO
if ([urlString rangeOfString:SensorsAnalyticsJavaScriptTrackEventScheme].location == NSNotFound) {
return NO;
} NSMutableDictionary *queryItems = [NSMutableDictionary dictionary];
// 请求中的所有 Query,并解析获取数据
NSArray<NSString *> *allQuery = [request.URL.query componentsSeparatedByString:@"&"];
for (NSString *query in allQuery) {
NSArray<NSString *> *items = [query componentsSeparatedByString:@"="];
if (items.count >= 2) {
queryItems[items.firstObject] = [items.lastObject stringByRemovingPercentEncoding];
}
}
//
[self trackFromH5WithEvent:queryItems[@"event"]]; return YES;
}

2.4 拦截

在 - webView: decidePolicyForNavigationAction: 代理方法中进行拦截

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if ([[SensorsAnalyticsSDK sharedInstance] shouldTrackWithWebView:webView request:navigationAction.request]) {
return decisionHandler(WKNavigationActionPolicyCancel);
}
decisionHandler(WKNavigationActionPolicyAllow);
}

2.5 测试验证

三、方案二:JavaScript 与 WebView 相互调用

​ 实现原理:在 WKWebView 控件初始化之后,通过调用 webView.configuration.userContentController 的 - addScriptMessageHandler:name: 方法注册回调,然后实现 WKScriptMessageHandler 协议中的 -userContentController:didReceiveScriptMessage: 方法,JavaScript SDK 通过 window.webkit.messageHandlers..postMessage()方式触发事件,我们就能在回调中接受到消息,然后从消息中解析事件信息,在调用 trackFromH5WithEvent:方法即可实现。

iOS全埋点解决方案-APP和H5打通的更多相关文章

  1. iOS全埋点解决方案-UITableView和UICollectionView点击事件

    前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...

  2. iOS全埋点解决方案-时间相关

    前言 ​ 我们使用"事件模型( Event 模型)"来描述用户的各种行为,事件模型包括事件( Event )和用户( User )两个核心实体.我们在描述用户行为时,往往只需要描述 ...

  3. iOS全埋点解决方案-应用退出和启动

    前言 ​ 通过应用程序退出事件,可以分析应用程序的平均使用时长:通过应用程序的启动事件,可以分析日活和新增.我们可以通过全埋点方式 SDK 实现应用程序的退出和启动事件. 一.全埋点的简介 ​ 目前. ...

  4. iOS全埋点解决方案-界面预览事件

    前言 ​ 我们先了解 UIViewController 生命周期相关的内容和 iOS 的"黑魔法" Method Swizzling.然后再了解页面浏览事件($AppViewScr ...

  5. iOS全埋点解决方案-控件点击事件

    前言 ​ 我们主要介绍如何实现控件点击事件($AppClick)的全埋点.在介绍如何实现之前,我们需要先了解一下,在 UIKit 框架下,处理点击或拖动事件的 Target-Action 设计模式. ...

  6. iOS全埋点解决方案-手势采集

    前言 ​ 随着科技以及业务的发展,手势的应用也越来越普及,因此对于数据采集,我们要考虑如果通过全埋点来实现手势的采集. 一.手势识别器 ​ 苹果为了降低开发者在手势事件处理方面的开发难度,定义了一个抽 ...

  7. iOS全埋点解决方案-采集奔溃

    前言 ​ 采集应用程序奔溃信息,主要分为以下两种场景: ​ NSException 异常 ​ Unix 信号异常 一.NSException 异常 ​ NSException 异常是 Objectiv ...

  8. iOS全埋点解决方案-数据存储

    前言 ​ SDK 需要把事件数据缓冲到本地,待符合一定策略再去同步数据. 一.数据存储策略 ​ 在 iOS 应用程序中,从 "数据缓冲在哪里" 这个纬度看,缓冲一般分两种类型. 内 ...

  9. 手淘架构组最新实践 | iOS基于静态库插桩的⼆进制重排启动优化 抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15% 编译期插桩

    抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15% 原创 Leo 字节跳动技术团队 2019-08-09 https://mp.weixin.qq.com/s/Drmmx5JtjG ...

随机推荐

  1. oracle system,sys用户 忘记密码,怎么修改密码

    sys用户是Oracle中权限最高的用户,而system是一个用于数据库管理的用户.在数据库安装完之后,应立即修改sys,system这两个用户的密码,以保证数据库的安全.但是我们有时候会遗忘密码或者 ...

  2. linux原生命令行看上面的内容

    上一页:Shift+pageUp 下一页:Shift+pageDown

  3. STL空间分配器源码分析(一)

    一.摘要 STL的空间分配器(allocator)定义于命名空间std内,主要为STL容器提供内存的分配和释放.对象的构造和析构的统一管理.空间分配器的实现细节,对于容器来说完全透明,容器不需关注内存 ...

  4. hutool工具类常用API整理

    0.官网学习地址 https://www.hutool.cn/ 1.依赖 <dependency> <groupId>cn.hutool</groupId> < ...

  5. Codeforces Round #677 (Div. 3) D. Districts Connection思维1200

    题目链接 Problem - 1433D - Codeforces 题目 美好的一天,从看不懂题目开始~~ Example input 4 5 1 2 2 1 3 3 1 1 1 4 1 1000 1 ...

  6. ElasticSearch7.3学习(二十一)----Filter与Query对比、使用explain关键字分析语法

    1.数据准备 首先创建book索引 PUT /book/ { "settings": { "number_of_shards": 1, "number ...

  7. JavaWeb和WebGIS学习笔记(五)——使用OpenLayers显示地图

    系列链接: Java web与web gis学习笔记(一)--Tomcat环境搭建 Java web与web gis学习笔记(二)--百度地图API调用 JavaWeb和WebGIS学习笔记(三)-- ...

  8. 最佳实践 | 通过使用 Jira Service Management 改进 HR 工作流程

    ​​ Jira Service Management 承诺解锁高速团队.技术团队和与之合作的业务部门都可以从 Jira Service Management中受益,尤其是 HR 团队. Atlassi ...

  9. S3C2410——LED灯实验

    一.S3C2410输入/输出的原理 Linux主要有字符设备.块设备和网络设备3类驱动程序,我们一般编写的驱动都是字符设备驱动程序. 二.程序部分 编写程序控制3个LED灯,代码分为2个部分:控制LE ...

  10. Vue组件之间通信

    vue组件传值有以下几种情况: 父组件向子组件传值.子组件向父组件传值.兄弟组件之间传值等 一.父组件向子组件传值: 传值方式: props <father> // 动态传递值 <s ...