前言

​ 我们使用“事件模型( 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全埋点解决方案-时间相关的更多相关文章

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    前言 ​ 所谓的 APP 和 H5 打通,是指 H5 集成 JavaScript 数据采集 SDK 后,H5 触发的事件不直接同步给服务器,而是先发给 APP 端的数据采集 SDK,经过 APP 端数 ...

  9. iOS全角符与半角符之间的转换

    iOS全角符与半角符之间的转换 相关资料: 函数『CFString​Transform』中文 详情: 问题 1.17-03-15,「有人在群里边问怎么把『半角』符字符串转换成『全角』字符串?」,百度的 ...

随机推荐

  1. 用纯CSS美化radio和checkbox

    Radio和checkbox需要美化吗?答案是必须的,因为设计风格一直都会变化,原生的样式百年不变肯定满足不了需求. 先看看纯CSS美化过后的radio和checkbox效果:查看. 项目地址:mag ...

  2. 从零到有模拟实现一个Set类

    前言 es6新增了Set数据结构,它允许你存储任何类型的唯一值,无论是原始值还是对象引用.这篇文章希望通过模拟实现一个Set来增加对它的理解. 原文链接 用在前面 实际工作和学习过程中,你可能也经常用 ...

  3. 将HTML页面转换为PDF文件并导出

    目前,在大多数的管理系统中,都会有这样一个功能:根据相关的条件查询相应的数据,并生成可视化报表,然后可导出为PDF文件.本文只展现生成可视化报表之后导出PDF文件的过程,生成可视化的报表可使用Echa ...

  4. ES6-11学习笔记--const

    新声明方式:const 1.不属于顶层对象 window 2.不允许重复声明 3.不存在变量提升 4.暂时性死区 5.块级作用域   以上特性跟let声明一样,特性可看 let 的学习笔记:链接跳转 ...

  5. 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 ...

  6. (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 行人再识别结 ...

  7. Python使用Odoo外部api

    Odoo服务器提供一个外部API,该API由其web客户端使用,也可以被支持XML-RPC或 JSON-RPC协议的编程语言(例如:Python.PHP.Ruby和Java)使用. 使用XML-RPC ...

  8. 为什么HashMap使用红黑树而不使用AVL树

    为什么HashMap使用红黑树而不使用AVL树? 红黑树适用于大量插入和删除:因为它是非严格的平衡树:只要从根节点到叶子节点的最长路径不超过最短路径的2倍,就不用进行平衡调节 AVL 树是严格的平衡树 ...

  9. 解决github上不去

    github上不去 在hosts文件中加入下列IP,保存即可生效. !!!!!注意!!!!! 网站对应的IP需要去[https://www.ipaddress.com/]网站查询, 可能与下面给出的不 ...

  10. Java中List接口重要实现类一ArrayList

    1.java.util.ArrayList 集合数据存储的结构是数组结构.元素查找快,而增删就比较慢 所以如果要查询数据,遍历数据,ArrayList是最常用的集合 2.ArrayList是不同步的, ...