From 914515733940c16d98412adba4981484fdd567b5 Mon Sep 17 00:00:00 2001 From: Alex Zolotarev Date: Thu, 6 Aug 2015 21:54:16 +0300 Subject: [alohalytics][ios] Measure total time spent in the app, get install/update/build/first launch date. --- .../examples/ios/SampleClient/AppDelegate.m | 6 ++ 3party/Alohalytics/src/alohalytics_objc.h | 20 +++- 3party/Alohalytics/src/apple/alohalytics_objc.mm | 115 +++++++++++++++++---- iphone/Maps/Classes/MapsAppDelegate.mm | 2 +- 4 files changed, 120 insertions(+), 23 deletions(-) diff --git a/3party/Alohalytics/examples/ios/SampleClient/AppDelegate.m b/3party/Alohalytics/examples/ios/SampleClient/AppDelegate.m index 3c702d333b..adffc904d4 100644 --- a/3party/Alohalytics/examples/ios/SampleClient/AppDelegate.m +++ b/3party/Alohalytics/examples/ios/SampleClient/AppDelegate.m @@ -44,6 +44,12 @@ // Used for example purposes only to upload statistics (unpredictable) in background, when system wakes app up. [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; + NSLog(@"isFirstSession: %d", [Alohalytics isFirstSession]); + NSLog(@"firstLaunchDate: %@", [Alohalytics firstLaunchDate]); + NSLog(@"installDate: %@", [Alohalytics installDate]); + NSLog(@"updateDate: %@", [Alohalytics updateDate]); + NSLog(@"buildDate: %@", [Alohalytics buildDate]); + return YES; } diff --git a/3party/Alohalytics/src/alohalytics_objc.h b/3party/Alohalytics/src/alohalytics_objc.h index 5396471b5b..922c79089c 100644 --- a/3party/Alohalytics/src/alohalytics_objc.h +++ b/3party/Alohalytics/src/alohalytics_objc.h @@ -23,11 +23,13 @@ *******************************************************************************/ // Convenience header to use from pure Objective-C project. +// TODO(AlexZ): Refactor it to use instance and it's methods instead of static functions. #ifndef ALOHALYTICS_OBJC_H #define ALOHALYTICS_OBJC_H #import +#import #import @interface Alohalytics : NSObject @@ -35,9 +37,6 @@ // Should be called in application:didFinishLaunchingWithOptions: or in application:willFinishLaunchingWithOptions: // Final serverUrl is modified to $(serverUrl)/[ios|mac]/your.bundle.id/app.version + (void)setup:(NSString *)serverUrl withLaunchOptions:(NSDictionary *)options; -// Alternative to the previous setup method if you integrated Alohalytics after initial release -// and don't want to count app upgrades as new installs (and definitely know that it's an already existing user). -+ (void)setup:(NSString *)serverUrl andFirstLaunch:(BOOL)isFirstLaunch withLaunchOptions:(NSDictionary *)options; + (void)forceUpload; + (void)logEvent:(NSString *)event; + (void)logEvent:(NSString *)event atLocation:(CLLocation *)location; @@ -48,6 +47,21 @@ + (void)logEvent:(NSString *)event withKeyValueArray:(NSArray *)array atLocation:(CLLocation *)location; + (void)logEvent:(NSString *)event withDictionary:(NSDictionary *)dictionary; + (void)logEvent:(NSString *)event withDictionary:(NSDictionary *)dictionary atLocation:(CLLocation *)location; +// Returns YES if it is a first session, before app goes into background. ++ (BOOL)isFirstSession; +// Returns summary time of all active user sessions up to now. ++ (NSInteger)totalSecondsSpentInTheApp; + +// Returns the date when app was launched for the first time (usually the same as install date). ++ (NSDate *)firstLaunchDate; +// When app was installed (it's Documents folder creation date). +// Note: firstLaunchDate is usually later than installDate. ++ (NSDate *)installDate; +// When app was updated (~== installDate for the first installation, it's Resources folder creation date). ++ (NSDate *)updateDate; +// When the binary was built. +// Hint: if buildDate > installDate then this is not a new app install, but an existing old user. ++ (NSDate *)buildDate; @end #endif // #ifndef ALOHALYTICS_OBJC_H diff --git a/3party/Alohalytics/src/apple/alohalytics_objc.mm b/3party/Alohalytics/src/apple/alohalytics_objc.mm index 6adc88f34a..b9820d7b7b 100644 --- a/3party/Alohalytics/src/apple/alohalytics_objc.mm +++ b/3party/Alohalytics/src/apple/alohalytics_objc.mm @@ -123,11 +123,9 @@ static Location ExtractLocation(CLLocation * l) { return extracted; } -// Returns string representing uint64_t timestamp of given file or directory (modification date in millis from 1970). -static std::string PathTimestampMillis(NSString * path) { - NSDictionary * attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; - if (attributes) { - NSDate * date = [attributes objectForKey:NSFileModificationDate]; +// Internal helper. +static std::string NSDateToMillisFrom1970(NSDate * date) { + if (date) { return std::to_string(static_cast([date timeIntervalSince1970] * 1000.)); } return std::string("0"); @@ -328,6 +326,14 @@ bool IsConnectionActive() { // Keys for NSUserDefaults. static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; +static NSString * const kFirstLaunchDateKey = @"AlohalyticsFirstLaunchDate"; +static NSString * const kTotalSecondsInTheApp = @"AlohalyticsTotalSecondsInTheApp"; + +// Used to calculate session length and total time spent in the app. +// setup should be called to activate counting. +static NSDate * gSessionStartTime = nil; +static BOOL gIsFirstSession = NO; + @implementation Alohalytics + (void)setDebugMode:(BOOL)enable { @@ -335,10 +341,6 @@ static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; } + (void)setup:(NSString *)serverUrl withLaunchOptions:(NSDictionary *)options { - [Alohalytics setup:serverUrl andFirstLaunch:YES withLaunchOptions:options]; -} - -+ (void)setup:(NSString *)serverUrl andFirstLaunch:(BOOL)isFirstLaunch withLaunchOptions:(NSDictionary *)options { const NSBundle * bundle = [NSBundle mainBundle]; NSString * bundleIdentifier = [bundle bundleIdentifier]; NSString * version = [[bundle infoDictionary] objectForKey:@"CFBundleShortVersionString"]; @@ -372,21 +374,26 @@ static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; NSUserDefaults * userDataBase = [NSUserDefaults standardUserDefaults]; NSString * installedVersion = [userDataBase objectForKey:kInstalledVersionKey]; BOOL shouldSendUpdatedSystemInformation = NO; - if (installationId.second && isFirstLaunch && installedVersion == nil) { - // Documents folder modification time can be interpreted as a "first app launch time" or an approx. "app install time". - // App bundle modification time can be interpreted as an "app update time". - instance.LogEvent("$install", {{"CFBundleShortVersionString", [version UTF8String]}, - {"documentsTimestampMillis", PathTimestampMillis([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject])}, - {"bundleTimestampMillis", PathTimestampMillis([bundle executablePath])}}); + // Do not generate $install event for old users who did not have Alohalytics installed but already used the app. + const BOOL appWasNeverInstalledAndLaunchedBefore = (NSOrderedAscending == [[Alohalytics buildDate] compare:[Alohalytics installDate]]); + if (installationId.second && appWasNeverInstalledAndLaunchedBefore && installedVersion == nil) { + gIsFirstSession = YES; + instance.LogEvent("$install", [Alohalytics bundleInformation:version]); [userDataBase setValue:version forKey:kInstalledVersionKey]; + // Also store first launch date for future use. + if (nil == [userDataBase objectForKey:kFirstLaunchDateKey]) { + [userDataBase setObject:[NSDate date] forKey:kFirstLaunchDateKey]; + } [userDataBase synchronize]; shouldSendUpdatedSystemInformation = YES; } else { if (installedVersion == nil || ![installedVersion isEqualToString:version]) { - instance.LogEvent("$update", {{"CFBundleShortVersionString", [version UTF8String]}, - {"documentsTimestampMillis", PathTimestampMillis([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject])}, - {"bundleTimestampMillis", PathTimestampMillis([bundle executablePath])}}); + instance.LogEvent("$update", [Alohalytics bundleInformation:version]); [userDataBase setValue:version forKey:kInstalledVersionKey]; + // Also store first launch date for future use, if Alohalytics was integrated only in this update. + if (nil == [userDataBase objectForKey:kFirstLaunchDateKey]) { + [userDataBase setObject:[NSDate date] forKey:kFirstLaunchDateKey]; + } [userDataBase synchronize]; shouldSendUpdatedSystemInformation = YES; } @@ -409,6 +416,14 @@ static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; #endif // TARGET_OS_IPHONE } ++(alohalytics::TStringMap)bundleInformation:(NSString *)version { + return {{"CFBundleShortVersionString", [version UTF8String]}, + {"installTimestampMillis", NSDateToMillisFrom1970([Alohalytics installDate])}, + {"updateTimestampMillis", NSDateToMillisFrom1970([Alohalytics updateDate])}, + {"buildTimestampMillis", NSDateToMillisFrom1970([Alohalytics buildDate])}, + }; +} + + (void)forceUpload { Stats::Instance().Upload(); } @@ -448,11 +463,24 @@ static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; #pragma mark App lifecycle notifications used to calculate basic metrics. #if (TARGET_OS_IPHONE > 0) + (void)applicationDidBecomeActive:(NSNotification *)notification { + gSessionStartTime = [NSDate date]; Stats::Instance().LogEvent("$applicationDidBecomeActive"); } + (void)applicationWillResignActive:(NSNotification *)notification { - Stats::Instance().LogEvent("$applicationWillResignActive"); + // Calculate session length. + NSInteger seconds = static_cast(-gSessionStartTime.timeIntervalSinceNow); + // nil it to filter time when the app is in the background, but totalSecondsSpentInTheApp is called. + gSessionStartTime = nil; + Stats & instance = Stats::Instance(); + instance.LogEvent("$applicationWillResignActive", std::to_string(seconds)); + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; + seconds += [defaults integerForKey:kTotalSecondsInTheApp]; + [defaults setInteger:seconds forKey:kTotalSecondsInTheApp]; + [defaults synchronize]; + if (instance.DebugMode()) { + ALOG("Total seconds spent in the app:", seconds); + } } + (void)applicationWillEnterForeground:(NSNotificationCenter *)notification { @@ -479,10 +507,59 @@ static NSString * const kInstalledVersionKey = @"AlohalyticsInstalledVersion"; ALOG("Skipped statistics uploading as connection is not active."); } } + if (gIsFirstSession) { + gIsFirstSession = NO; + } } + (void)applicationWillTerminate:(NSNotification *)notification { Stats::Instance().LogEvent("$applicationWillTerminate"); } #endif // TARGET_OS_IPHONE + +#pragma mark Utility methods + ++ (BOOL)isFirstSession { + return gIsFirstSession; +} + ++ (NSDate *)firstLaunchDate { + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; + NSDate * date = [defaults objectForKey:kFirstLaunchDateKey]; + if (!date) { + // Non-standard situation: this method is called before calling setup. Return current date. + date = [NSDate date]; + [defaults setObject:date forKey:kFirstLaunchDateKey]; + [defaults synchronize]; + } + return date; +} + ++ (NSInteger)totalSecondsSpentInTheApp { + NSInteger seconds = [[NSUserDefaults standardUserDefaults] integerForKey:kTotalSecondsInTheApp]; + // Take into an account currently active session. + if (gSessionStartTime) { + seconds += static_cast(-gSessionStartTime.timeIntervalSinceNow); + } + return seconds; +} + +// Internal helper, returns nil for invalid paths. ++ (NSDate *)fileCreationDate:(NSString *)fullPath { + NSDictionary * attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil]; + return attributes ? [attributes objectForKey:NSFileCreationDate] : nil; +} + ++ (NSDate *)installDate { + return [Alohalytics fileCreationDate:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]]; +} + ++ (NSDate *)updateDate { + return [Alohalytics fileCreationDate:[[NSBundle mainBundle] resourcePath]]; +} + ++ (NSDate *)buildDate { + return [Alohalytics fileCreationDate:[[NSBundle mainBundle] executablePath]]; +} + @end diff --git a/iphone/Maps/Classes/MapsAppDelegate.mm b/iphone/Maps/Classes/MapsAppDelegate.mm index 1696125f4e..3c30d56708 100644 --- a/iphone/Maps/Classes/MapsAppDelegate.mm +++ b/iphone/Maps/Classes/MapsAppDelegate.mm @@ -167,7 +167,7 @@ void InitLocalizedStrings() #ifndef OMIM_PRODUCTION [Alohalytics setDebugMode:YES]; #endif - [Alohalytics setup:@"http://localhost:8080" andFirstLaunch:[MapsAppDelegate isFirstAppLaunch] withLaunchOptions:launchOptions]; + [Alohalytics setup:@"http://localhost:8080" withLaunchOptions:launchOptions]; NSURL *url = launchOptions[UIApplicationLaunchOptionsURLKey]; if (url != nil) -- cgit v1.2.3