前言
我們使用“事件模型( 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 個問題:
- 如何計算最好一個頁面的介面預覽事件時長
- 如何處理巢狀子頁面的預覽事件時長
具體實現:後續介紹