[alohalytics][ios] Measure total time spent in the app, get install/update/build/first launch date.

This commit is contained in:
Alex Zolotarev 2015-08-06 21:54:16 +03:00
parent 0cd24f656e
commit 9145157339
4 changed files with 120 additions and 23 deletions

View file

@ -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;
}

View file

@ -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 <Foundation/Foundation.h>
#import <Foundation/NSDate.h>
#import <CoreLocation/CoreLocation.h>
@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

View file

@ -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<uint64_t>([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<NSInteger>(-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<NSInteger>(-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

View file

@ -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)