iOS全埋点解决方案-时间相关
前言
我们使用“事件模型( Event 模型)”来描述用户的各种行为,事件模型包括事件( Event )和用户( User )两个核心实体。我们在描述用户行为时,往往只需要描述清楚几个要点,即可将整个行为描述清楚,要点
包括:是谁、什么时间、什么地点、以什么方式、干了什么。而事件( Event )和用户( User )这两个实体结合在一起就可以达到这一目的。
Event 实体
一个完整的事件( Event ),包含如下的几个关键因素:
Who:即参与这个事件的用户是谁。
When:即这个事件发生的实际时间。
Where:即事件发生的地点。
How:即用户从事这个事件的方式。这个概念就比较广了,包括用户使用的设备、使用的浏览器、使用的 App 版本、操作系统版本、进入的渠道、跳转过来时的 referer 等,目前,神策分析预置了如下字段用来描述这类信息,使用者也可以根据自己的需要来增加相应的自定义字段。
What:以字段的方式记录用户所做的事件的具体内容。
$app_version:应用版本
$city:城市
$manufacturer:设备制造商,字符串类型,如"Apple"
$model:设备型号,字符串类型,如"iphone6"
$os:操作系统,字符串类型,如"iOS"
$os_version:操作系统版本,字符串类型,如"8.1.1"
$screen_height:屏幕高度,数字类型,如 1920
$screen_width:屏幕宽度,数字类型,如 1080
$wifi:是否 WIFI,BOOL 类型,如 true
User 实体
每个 User 实体对应一个真实的用户,每个用户有各种属性,常见的属性例如:年龄、性别,和业务相关的属性则可能有:会员等级、当前积分、好友数等等。这些描述用户的字段,就是用户属性。
接下来我们主要说的 When 这个因素,即时间。包括事件发生的时间戳和统计事件持续的时长。
一、事件发生的时间戳
时间纠正:如果 T2 和 T3 相差太大,我们可以确定当前用户手机的时间戳是不准确的,及比服务器的时间晚了一个小时,因此,我们认为事件发生的时间 T1 也晚了一个小时,这就达到时间纠正的效果。
二、统计事件持续时长
事件持续时长,是用来统计用户的某个行为或者动作持续了多次事件(比如,观看了某个视频)的。统计事件持续时长,就像一个计时器,当用户的某个行为或者动作发生时,就开始计时;当行为或者动作结束时就停止计时,这个 事件间隔(在事件中,我们用 $event_duration 来表示 )为用户发生这个行为或者动作的持续时长。
2.1 实现步骤
为了方便统计时长,我们需要新增两个方法:
开始计时:- trackTimerStart:
停止计时:-trackTimerEnd:properties:
当某个行为或者活动开始时,调用 - trackTimerStart:开始计时,此时并不会触发事件,仅仅是 SDK 内部记录耨个事件的开始的时间戳。当这个行为或者活动结束时,调用 -trackTimerEnd:properties:结束计时器,然后 SDK 计算持续时长 $event_duration 属性的值并触发事件。
实现步骤:
第一步:新增 SensorsAnalyticsSDK 文件的类别 Timer ,并新增 - trackTimerStart: 和 - trackTimerEnd: properties: 方法的声明
#import <SensorsSDK/SensorsSDK.h>
NS_ASSUME_NONNULL_BEGIN
@interface SensorsAnalyticsSDK (Timer)
/// 开始统计事件时长
/// @param event 事件名
- (void)trackTimerStart:(NSString *)event;
/// 结束事件时长统计,计算时长
/// @param event 事件名 与开始时事件名一一对应
/// @param properties 事件属性
- (void)trackTimerEnd:(NSString *)event properties:(nullable NSDictionary *) properties;
@end
NS_ASSUME_NONNULL_END
第二步:在 SensorsAnalyticsSDK 中新增 trackTimer 属性,用于记录事件开始发生的时间戳,并在 - init 方法中进行初始化。
/// 事件开始发生的时间戳
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSDictionary *> *trackTimer;
- (instancetype)init {
self = [super init];
if (self) {
_automaticProperties = [self collectAutomaticProperties];
// 设置是否需是被动启动标记
_launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
_loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
_trackTimer = [NSMutableDictionary dictionary];
// 添加应用程序状态监听
[self setupListeners];
}
return self;
}
第三步:在 SensorsAnalyticsSDK 文件中新增 + currentTime 方法,用于获取用户当期的时间戳。
// 获取手机当前时间戳
+ (double)currentTime {
return [[NSDate date] timeIntervalSince1970] * 1000;
}
第四步:在 SensorsAnalyticsSDK+Timer 类别中实现 - trackTimerStart: 和 - trackTimerEnd: properties: 方法
#import "SensorsAnalyticsSDK+Timer.h"
static NSString * const SensorsAnalyticsEventBeginKey = @"event_begin";
@implementation SensorsAnalyticsSDK (Timer)
- (void)trackTimerStart:(NSString *)event {
self.trackTimer[event] = @{SensorsAnalyticsEventBeginKey: @([SensorsAnalyticsSDK currentTime])};
}
- (void)trackTimerEnd:(NSString *)event properties:(NSDictionary *)properties {
NSDictionary *evnetTimer = self.trackTimer[event];
if (!evnetTimer) {
return [self track:event properties:properties];
}
NSMutableDictionary *p = [NSMutableDictionary dictionaryWithDictionary:properties];
// 移除
[self.trackTimer removeObjectForKey:event];
// 事件开始时间
double beginTime = [(NSNumber *)evnetTimer[SensorsAnalyticsEventBeginKey] doubleValue];
// 获取当前系统事件
double currentTime = [SensorsAnalyticsSDK currentTime];
// 计算事件时长
double eventDuration = currentTime - beginTime;
eventDuration = [[NSString stringWithFormat:@"%.3lf", eventDuration] floatValue];
// 设置事件时长属性
[p setObject:@(eventDuration) forKey:@"$event_duration"];
// 触发事件
[self track:event properties:p];
}
@end
第五步:测试验证
[[SensorsAnalyticsSDK sharedInstance] trackTimerStart:@"doSomething"];
[[SensorsAnalyticsSDK sharedInstance] trackTimerEnd:@"doSomething" properties:nil];
{
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$event_duration" : 2046.623046875,
"$app_version" : "1.0",
"$os_version" : "15.2",
"$lib" : "iOS"
},
"event" : "doSomething",
"time" : 1649382527225,
"distinct_id" : "1234567"
}
可能存在问题: 如果调用 - trackTimerStart: 和 - trackTimerEnd: properties: 方法之间用户调整了手机时间,可能会出现下面的问题
- 统计的 $event_duration 可能接近于0
- 统计的 $event_duration 可能非常大,可能超过一个月
- 统计的 $event_duration 可能为负数
这是因为我们目前是借助手机客户端的时间来计算 $event_duration 的,一旦用户调整了手机的时间,必然会影响 $event_duration 属性的计算。
解决方法:引入 systemUpTime(系统启动事件,也叫开机时间), 指设备开机后一共运行了多少秒(设备休眠不同统计在内),并且不会受到系统时间更改影响。我们可以使用 systemUpTime 来计算 $event_duration 属性。
在 SensorsAnalyticsSDK 文件中新增 + systemUpTime 方法
// 系统启动时间
+ (double)systemUpTime {
return NSProcessInfo.processInfo.systemUptime * 1000;
}
将 SensorsAnalyticsSDK+Timer 中 调用 + currentTime 改成 + systemUpTime 方法。至此就解决了事件持续时长统计不准确的问题。
2.2 事件的暂停和恢复
引入事件暂停和恢复的方法:
暂停统计时长方法:- trackTimerPause:
恢复统计时长方法:- trackTimerResume:
实现步骤:
第一步:在 SensorsAnalyticsSDK+Timer 文件中新增 - trackTimerPause: - trackTimerResume: 方法
@interface SensorsAnalyticsSDK (Timer)
/// 暂停事件统计时长
/// @param event 事件名
- (void)trackTimerPause:(NSString *)event;
/// 恢复事件统计时长
/// @param event 事件名
- (void)trackTimerResume:(NSString *)event;
@end
static NSString * const SensorsAnalyticsEventDurationKey = @"event_duration";
static NSString * const SensorsAnalyticsEventIsPauseKey = @"is_pause";
- (void)trackTimerPause:(NSString *)event {
NSMutableDictionary *eventTimer = [self.trackTimer[event] mutableCopy];
// 如果没有开始,直接返回
if (!eventTimer) {
return;
}
// 如果该事件时长统计已经结束,直接返回,不做任何处理
if ([eventTimer[SensorsAnalyticsEventIsPauseKey] boolValue]) {
return;
}
// 获取当前系统启动时间
double systemUpTime = [SensorsAnalyticsSDK systemUpTime];
// 获取事件开始时间
double beginTime = [eventTimer[SensorsAnalyticsEventBeginKey] doubleValue];
// 计算暂停前统计的时长
double duration = [eventTimer[SensorsAnalyticsEventDurationKey] doubleValue] + systemUpTime - beginTime;
eventTimer[SensorsAnalyticsEventDurationKey] = @(duration);
// 事件处于暂停状态
eventTimer[SensorsAnalyticsEventIsPauseKey] = @(YES);
self.trackTimer[event] = eventTimer;
}
- (void)trackTimerResume:(NSString *)event {
NSMutableDictionary *eventTimer = [self.trackTimer[event] mutableCopy];
// 如果没有开始,直接返回
if (!eventTimer) {
return;
}
// 如果该事件时长统计没有暂停,直接返回,不做任何处理
if ([eventTimer[SensorsAnalyticsEventIsPauseKey] boolValue]) {
return;
}
// 获取当前系统启动时间
double systemUpTime = [SensorsAnalyticsSDK systemUpTime];
// 重置事件开始事件
eventTimer[SensorsAnalyticsEventBeginKey] = @(systemUpTime);
// 将事件暂停被标记设置为 NO
eventTimer[SensorsAnalyticsEventIsPauseKey] = @(NO);
self.trackTimer[event] = eventTimer;
}
第二步:修改 - trackTimerEnd: properties: 方法
- (void)trackTimerEnd:(NSString *)event properties:(NSDictionary *)properties {
NSDictionary *eventTimer = self.trackTimer[event];
if (!eventTimer) {
return [self track:event properties:properties];
}
NSMutableDictionary *p = [NSMutableDictionary dictionaryWithDictionary:properties];
// 移除
[self.trackTimer removeObjectForKey:event];
if ([eventTimer[SensorsAnalyticsEventIsPauseKey] boolValue]) {
// 获取事件时长
double eventDuration = [eventTimer[SensorsAnalyticsEventDurationKey] doubleValue];
// 设置事件时长属性
p[@"$event_duration"] = @([[NSString stringWithFormat:@"%.3lf", eventDuration] floatValue]);
} else {
// 事件开始时间
double beginTime = [(NSNumber *)eventTimer[SensorsAnalyticsEventBeginKey] doubleValue];
// 获取当前系统事件
double currentTime = [SensorsAnalyticsSDK systemUpTime];
// 计算事件时长
double eventDuration = currentTime - beginTime + [eventTimer[SensorsAnalyticsEventDurationKey] doubleValue];
eventDuration = [[NSString stringWithFormat:@"%.3lf", eventDuration] floatValue];
// 设置事件时长属性
[p setObject:@(eventDuration) forKey:@"$event_duration"];
}
// 触发事件
[self track:event properties:p];
}
第三步:测试验证
{
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$event_duration" : 1663.958984375,
"$app_version" : "1.0",
"$os_version" : "15.2",
"$lib" : "iOS"
},
"event" : "doSomething",
"time" : 1649398840807,
"distinct_id" : "1234567"
}
2.3 后台状态下的事件时长
以上问题:当应用程序进入后台后,由于我们是通过记录事件开始时间,然后在事件结束时,计算时间差来计算事件的持续时长 ,包括了进入后台的时间。因为在应用程序进入后台时,我们应该调用暂停的方法,当应用程序回到前台运行时,我们调用恢复事件方法。
实现步骤:
第一步:在 SensorsAnalyticsSDK 文件中新增一个属性 enterBackgroundTrackTimerEvents 用来保存进入后台时未暂停的事件名。然后在 -init 方法中进行初始化
/// 保存进入后台时未暂停的事件名称
@property (nonatomic, strong) NSMutableArray<NSString *> *enterBackgroundTrackTimerEvents;
- (instancetype)init {
self = [super init];
if (self) {
_automaticProperties = [self collectAutomaticProperties];
// 设置是否需是被动启动标记
_launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
_loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
_trackTimer = [NSMutableDictionary dictionary];
_enterBackgroundTrackTimerEvents = [NSMutableArray array];
// 添加应用程序状态监听
[self setupListeners];
}
return self;
}
第二步:在应用程序进入后台时,调用暂停方法,将所有未暂停的事件暂停
- (void)applicationDidEnterBackground:(NSNotification *)notification {
NSLog(@"Application did enter background.");
// 还原标记位
self.applicationWillResignActive = NO;
// 触发 AppEnd 事件
[self track:@"$AppEnd" properties:nil];
// 暂停所有事件时长统计
[self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
if (![obj[@"is_pause"] boolValue]) {
[self.enterBackgroundTrackTimerEvents addObject:key];
[self trackTimerPause:key];
}
}];
}
第三步:在应用程序进入前台的时候,调用事件恢复启动
- (void)applicationDidBecomeActive:(NSNotification *)notification {
NSLog(@"Application did enter active.");
// 还原标记位
if (self.applicationWillResignActive) {
self.applicationWillResignActive = NO;
return;
}
// 将被动启动标记位设置为 NO,正常记录事件
self.launchedPassively = NO;
// 触发 AppStart 事件
[self track:@"$AppStart" properties:nil];
// 恢复所有的事件时长统计
for (NSString *event in self.enterBackgroundTrackTimerEvents) {
[self trackTimerStart:event];
}
[self.enterBackgroundTrackTimerEvents removeAllObjects];
}
第四步:测试运行
三、全埋点事件时长
3.1 $AppEnd 事件时长
当收到 UIApplicationDidBecomeActiveNotification 本地通知时,调用 - trackTimerStart:方法开始计时,当收到 UIApplicationDidEnterBackgroundNotification 本地通知时,调用 - track: properties: 方法结束计时。
实现步骤:
第一步:修改 - applicationDidBecomeActive:方法,在结束时调用 - trackTimerStart:方法
- (void)applicationDidBecomeActive:(NSNotification *)notification {
NSLog(@"Application did enter active.");
// 还原标记位
if (self.applicationWillResignActive) {
self.applicationWillResignActive = NO;
return;
}
// 将被动启动标记位设置为 NO,正常记录事件
self.launchedPassively = NO;
// 触发 AppStart 事件
[self track:@"$AppStart" properties:nil];
// 恢复所有的事件时长统计
for (NSString *event in self.enterBackgroundTrackTimerEvents) {
[self trackTimerStart:event];
}
[self.enterBackgroundTrackTimerEvents removeAllObjects];
// 开始 $AppEnd 事件计时
[self trackTimerStart:@"$AppEnd"];
}
第二步:修改 - applicationDidEnterBackground: 方法,将 [self track:@"$AppEnd" properties:nil]; 修改成 [self trackTimerEnd:@"$AppEnd" properties:nil];
- (void)applicationDidEnterBackground:(NSNotification *)notification {
NSLog(@"Application did enter background.");
// 还原标记位
self.applicationWillResignActive = NO;
// 触发 AppEnd 事件
// [self track:@"$AppEnd" properties:nil];
[self trackTimerEnd:@"$AppEnd" properties:nil];
// 暂停所有事件时长统计
[self.trackTimer enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) {
if (![obj[@"is_pause"] boolValue]) {
[self.enterBackgroundTrackTimerEvents addObject:key];
[self trackTimerPause:key];
}
}];
}
第三步:测试验证
{
"propeerties" : {
"$model" : "x86_64",
"$manufacturer" : "Apple",
"$lib_version" : "1.0.0",
"$os" : "iOS",
"$event_duration" : 16705.58984375,
"$app_version" : "1.0",
"$os_version" : "15.2",
"$lib" : "iOS"
},
"event" : "$AppEnd",
"time" : 1649402996456,
"distinct_id" : "1234567"
}
3.2 $AppViewScreen 时间时长
如果按照 $AppEnd 方式实现 $AppViewScreen 时间时长,可能会存在 2 个问题:
- 如何计算最好一个页面的界面预览事件时长
- 如何处理嵌套子页面的预览事件时长
具体实现:后续介绍
iOS全埋点解决方案-时间相关的更多相关文章
- iOS全埋点解决方案-UITableView和UICollectionView点击事件
前言 在 $AppClick 事件采集中,还有两个比较特殊的控件: UITableView •UICollectionView 这两个控件的点击事件,一般指的是点击 UITableViewCell 和 ...
- iOS全埋点解决方案-应用退出和启动
前言 通过应用程序退出事件,可以分析应用程序的平均使用时长:通过应用程序的启动事件,可以分析日活和新增.我们可以通过全埋点方式 SDK 实现应用程序的退出和启动事件. 一.全埋点的简介 目前. ...
- iOS全埋点解决方案-界面预览事件
前言 我们先了解 UIViewController 生命周期相关的内容和 iOS 的"黑魔法" Method Swizzling.然后再了解页面浏览事件($AppViewScr ...
- iOS全埋点解决方案-控件点击事件
前言 我们主要介绍如何实现控件点击事件($AppClick)的全埋点.在介绍如何实现之前,我们需要先了解一下,在 UIKit 框架下,处理点击或拖动事件的 Target-Action 设计模式. ...
- iOS全埋点解决方案-手势采集
前言 随着科技以及业务的发展,手势的应用也越来越普及,因此对于数据采集,我们要考虑如果通过全埋点来实现手势的采集. 一.手势识别器 苹果为了降低开发者在手势事件处理方面的开发难度,定义了一个抽 ...
- iOS全埋点解决方案-采集奔溃
前言 采集应用程序奔溃信息,主要分为以下两种场景: NSException 异常 Unix 信号异常 一.NSException 异常 NSException 异常是 Objectiv ...
- iOS全埋点解决方案-数据存储
前言 SDK 需要把事件数据缓冲到本地,待符合一定策略再去同步数据. 一.数据存储策略 在 iOS 应用程序中,从 "数据缓冲在哪里" 这个纬度看,缓冲一般分两种类型. 内 ...
- iOS全埋点解决方案-APP和H5打通
前言 所谓的 APP 和 H5 打通,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务器,而是先发给 APP 端的数据采集 SDK,经过 APP 端数 ...
- iOS全角符与半角符之间的转换
iOS全角符与半角符之间的转换 相关资料: 函数『CFStringTransform』中文 详情: 问题 1.17-03-15,「有人在群里边问怎么把『半角』符字符串转换成『全角』字符串?」,百度的 ...
随机推荐
- 用纯CSS美化radio和checkbox
Radio和checkbox需要美化吗?答案是必须的,因为设计风格一直都会变化,原生的样式百年不变肯定满足不了需求. 先看看纯CSS美化过后的radio和checkbox效果:查看. 项目地址:mag ...
- 从零到有模拟实现一个Set类
前言 es6新增了Set数据结构,它允许你存储任何类型的唯一值,无论是原始值还是对象引用.这篇文章希望通过模拟实现一个Set来增加对它的理解. 原文链接 用在前面 实际工作和学习过程中,你可能也经常用 ...
- 将HTML页面转换为PDF文件并导出
目前,在大多数的管理系统中,都会有这样一个功能:根据相关的条件查询相应的数据,并生成可视化报表,然后可导出为PDF文件.本文只展现生成可视化报表之后导出PDF文件的过程,生成可视化的报表可使用Echa ...
- ES6-11学习笔记--const
新声明方式:const 1.不属于顶层对象 window 2.不允许重复声明 3.不存在变量提升 4.暂时性死区 5.块级作用域 以上特性跟let声明一样,特性可看 let 的学习笔记:链接跳转 ...
- Idea运行时Scala报错Exception in thread "main" java.lang.NoSuchMethodError:com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V
一.情况描述 使用idea +scala+spark,运行程序代码如下: package cn.idcast.hello import org.apache.spark.rdd.RDD import ...
- (ICONIP2021)On the Unreasonable Effectiveness of Centroids in Image
目录 摘要 1.引言 2.提出的方法 2.1 CentroidTripletloss 2.2 聚合表示 3.实验 3.1 数据集 3.2 应用细节 3.3 Fashion检索结果 3.4 行人再识别结 ...
- Python使用Odoo外部api
Odoo服务器提供一个外部API,该API由其web客户端使用,也可以被支持XML-RPC或 JSON-RPC协议的编程语言(例如:Python.PHP.Ruby和Java)使用. 使用XML-RPC ...
- 为什么HashMap使用红黑树而不使用AVL树
为什么HashMap使用红黑树而不使用AVL树? 红黑树适用于大量插入和删除:因为它是非严格的平衡树:只要从根节点到叶子节点的最长路径不超过最短路径的2倍,就不用进行平衡调节 AVL 树是严格的平衡树 ...
- 解决github上不去
github上不去 在hosts文件中加入下列IP,保存即可生效. !!!!!注意!!!!! 网站对应的IP需要去[https://www.ipaddress.com/]网站查询, 可能与下面给出的不 ...
- Java中List接口重要实现类一ArrayList
1.java.util.ArrayList 集合数据存储的结构是数组结构.元素查找快,而增删就比较慢 所以如果要查询数据,遍历数据,ArrayList是最常用的集合 2.ArrayList是不同步的, ...