草庐IT

iOS全埋点解决方案-APP和H5打通

任伟 2023-03-28 原文

前言

​ 所谓的 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. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  3. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  4. ruby-on-rails - 如何重命名或移动 Rails 的 README_FOR_APP - 2

    当我在我的Rails应用程序根目录中运行rakedoc:app时,API文档是使用/doc/README_FOR_APP作为主页生成的。我想向该文件添加.rdoc扩展名,以便它在GitHub上正确呈现。更好的是,我想将它移动到应用程序根目录(/README.rdoc)。有没有办法通过修改包含的rake/rdoctask任务在我的Rakefile中执行此操作?是否有某个地方可以查找可以修改的主页文件的名称?还是我必须编写一个新的Rake任务?额外的问题:Rails应用程序的两个单独文件/README和/doc/README_FOR_APP背后的逻辑是什么?为什么不只有一个?

  5. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  6. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  7. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  8. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  9. ruby - 使用 postgres.app 在 rvm 下要求 pg 时出错 - 2

    我正在使用Postgres.app在OSX(10.8.3)上。我已经修改了我的PATH,以便应用程序的bin文件夹位于所有其他文件夹之前。Rammy:~phrogz$whichpg_config/Applications/Postgres.app/Contents/MacOS/bin/pg_config我已经安装了rvm并且可以毫无错误地安装pggem,但是当我需要它时我得到一个错误:Rammy:~phrogz$gem-v1.8.25Rammy:~phrogz$geminstallpgFetching:pg-0.15.1.gem(100%)Buildingnativeextension

  10. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

随机推荐