iOS全埋點解決方案-時間相關

任淏發表於2022-05-15

前言

image-20220407143344033

​ 我們使用“事件模型( 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 這個因素,即時間。包括事件發生的時間戳和統計事件持續的時長。

一、事件發生的時間戳

image-20220407145213360

​ 時間糾正:如果 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 個問題:

  • 如何計算最好一個頁面的介面預覽事件時長
  • 如何處理巢狀子頁面的預覽事件時長

具體實現:後續介紹

相關文章