|
|
|
@ -1,12 +1,53 @@
|
|
|
|
|
//
|
|
|
|
|
// OWSAnalytics.m
|
|
|
|
|
//
|
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
#import "OWSAnalytics.h"
|
|
|
|
|
#import "AppVersion.h"
|
|
|
|
|
#import "TSStorageManager.h"
|
|
|
|
|
#import <CocoaLumberjack/CocoaLumberjack.h>
|
|
|
|
|
#import <Reachability/Reachability.h>
|
|
|
|
|
|
|
|
|
|
#import "OWSAnalytics.h"
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
|
|
|
|
|
|
|
|
#define NO_SIGNAL_ANALYTICS
|
|
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
|
|
|
|
|
// TODO: Disable analytics for debug builds.
|
|
|
|
|
//#define NO_SIGNAL_ANALYTICS
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
NSString *const kOWSAnalytics_EventsCollection = @"kOWSAnalytics_EventsCollection";
|
|
|
|
|
|
|
|
|
|
NSString *const kOWSAnalytics_Collection = @"kOWSAnalytics_Collection";
|
|
|
|
|
NSString *const kOWSAnalytics_KeyLaunchCount = @"kOWSAnalytics_KeyLaunchCount";
|
|
|
|
|
NSString *const kOWSAnalytics_KeyLaunchCompleteCount = @"kOWSAnalytics_KeyLaunchCompleteCount";
|
|
|
|
|
|
|
|
|
|
// Percentage of analytics events to discard. 0 <= x <= 100.
|
|
|
|
|
const int kOWSAnalytics_DiscardFrequency = 0;
|
|
|
|
|
|
|
|
|
|
@interface OWSAnalytics ()
|
|
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) TSStorageManager *storageManager;
|
|
|
|
|
@property (nonatomic, readonly) Reachability *reachability;
|
|
|
|
|
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
|
|
|
|
|
|
|
|
|
@property (atomic) BOOL hasRequestInFlight;
|
|
|
|
|
|
|
|
|
|
@property (atomic) NSNumber *launchCount;
|
|
|
|
|
@property (atomic) NSNumber *launchCompleteCount;
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
|
|
@implementation OWSAnalytics
|
|
|
|
|
|
|
|
|
@ -16,27 +57,223 @@
|
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
|
instance = [self new];
|
|
|
|
|
// TODO: If we ever log these events to disk,
|
|
|
|
|
// we may want to protect these file(s) like TSStorageManager.
|
|
|
|
|
instance = [[self alloc] initDefault];
|
|
|
|
|
});
|
|
|
|
|
return instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (void)logEvent:(NSString *)description
|
|
|
|
|
severity:(OWSAnalyticsSeverity)severity
|
|
|
|
|
parameters:(NSDictionary *)parameters
|
|
|
|
|
location:(const char *)location
|
|
|
|
|
- (instancetype)initDefault
|
|
|
|
|
{
|
|
|
|
|
TSStorageManager *storageManager = [TSStorageManager sharedManager];
|
|
|
|
|
|
|
|
|
|
return [self initWithStorageManager:storageManager];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager
|
|
|
|
|
{
|
|
|
|
|
self = [super init];
|
|
|
|
|
|
|
|
|
|
if (!self) {
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OWSAssert(storageManager);
|
|
|
|
|
|
|
|
|
|
_storageManager = storageManager;
|
|
|
|
|
// Use a newDatabaseConnection so as not to block other reads in the launch path.
|
|
|
|
|
_dbConnection = storageManager.newDatabaseConnection;
|
|
|
|
|
_reachability = [Reachability reachabilityForInternetConnection];
|
|
|
|
|
|
|
|
|
|
[self observeNotifications];
|
|
|
|
|
|
|
|
|
|
OWSSingletonAssert();
|
|
|
|
|
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)observeNotifications
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(reachabilityChanged)
|
|
|
|
|
name:kReachabilityChangedNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
|
|
|
selector:@selector(applicationDidBecomeActive)
|
|
|
|
|
name:UIApplicationDidBecomeActiveNotification
|
|
|
|
|
object:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
|
{
|
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)reachabilityChanged
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[self tryToSyncEvents];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)applicationDidBecomeActive
|
|
|
|
|
{
|
|
|
|
|
OWSAssert([NSThread isMainThread]);
|
|
|
|
|
|
|
|
|
|
[self tryToSyncEvents];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)tryToSyncEvents
|
|
|
|
|
{
|
|
|
|
|
// Don't try to sync if:
|
|
|
|
|
//
|
|
|
|
|
// * There's no network available.
|
|
|
|
|
// * There's already a sync request in flight.
|
|
|
|
|
if (!self.reachability.isReachable || self.hasRequestInFlight) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispatch_async(self.serialQueue, ^{
|
|
|
|
|
__block NSString *firstEventKey = nil;
|
|
|
|
|
__block NSDictionary *firstEventDictionary = nil;
|
|
|
|
|
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
// Take any event. We don't need to deliver them in any particular order.
|
|
|
|
|
[transaction enumerateKeysInCollection:kOWSAnalytics_EventsCollection
|
|
|
|
|
usingBlock:^(NSString *key, BOOL *_Nonnull stop) {
|
|
|
|
|
firstEventKey = key;
|
|
|
|
|
*stop = YES;
|
|
|
|
|
}];
|
|
|
|
|
if (!firstEventKey) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
firstEventDictionary = [transaction objectForKey:firstEventKey inCollection:kOWSAnalytics_EventsCollection];
|
|
|
|
|
OWSAssert(firstEventDictionary);
|
|
|
|
|
OWSAssert([firstEventDictionary isKindOfClass:[NSDictionary class]]);
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
if (!firstEventDictionary) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DDLogDebug(@"%@ trying to deliver event: %@", self.tag, firstEventKey);
|
|
|
|
|
self.hasRequestInFlight = YES;
|
|
|
|
|
// Until we integrate with an analytics platform, behave as though all event delivery succeeds.
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
self.hasRequestInFlight = NO;
|
|
|
|
|
|
|
|
|
|
BOOL success = YES;
|
|
|
|
|
if (success) {
|
|
|
|
|
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
|
// Remove from queue.
|
|
|
|
|
[transaction removeObjectForKey:firstEventKey inCollection:kOWSAnalytics_EventsCollection];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait a second between network requests / retries.
|
|
|
|
|
dispatch_after(
|
|
|
|
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
|
[self tryToSyncEvents];
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (dispatch_queue_t)serialQueue
|
|
|
|
|
{
|
|
|
|
|
static dispatch_queue_t queue = nil;
|
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
|
queue = dispatch_queue_create("org.whispersystems.analytics.serial", DISPATCH_QUEUE_SERIAL);
|
|
|
|
|
});
|
|
|
|
|
return queue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSDictionary<NSString *, id> *)eventSuperProperties
|
|
|
|
|
{
|
|
|
|
|
NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary new];
|
|
|
|
|
if (AppVersion.instance.firstAppVersion) {
|
|
|
|
|
result[@"app_version_first"] = AppVersion.instance.firstAppVersion;
|
|
|
|
|
}
|
|
|
|
|
if (AppVersion.instance.lastAppVersion) {
|
|
|
|
|
result[@"app_version_last"] = AppVersion.instance.lastAppVersion;
|
|
|
|
|
}
|
|
|
|
|
if (AppVersion.instance.currentAppVersion) {
|
|
|
|
|
result[@"app_version_current"] = AppVersion.instance.currentAppVersion;
|
|
|
|
|
}
|
|
|
|
|
NSNumber *launchCount = self.launchCount;
|
|
|
|
|
if (launchCount) {
|
|
|
|
|
result[@"launch_count"] = @([self orderOfMagnitudeOf:launchCount.longValue]);
|
|
|
|
|
}
|
|
|
|
|
// TODO: Order of magnitude: thread count.
|
|
|
|
|
// TODO: Order of magnitude: total message count.
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (long)orderOfMagnitudeOf:(long)value
|
|
|
|
|
{
|
|
|
|
|
return [OWSAnalytics orderOfMagnitudeOf:value];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (long)orderOfMagnitudeOf:(long)value
|
|
|
|
|
{
|
|
|
|
|
if (value <= 0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return (long)round(pow(10, floor(log10(value))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)addEvent:(NSString *)eventName properties:(NSDictionary *)properties
|
|
|
|
|
{
|
|
|
|
|
OWSAssert(eventName.length > 0);
|
|
|
|
|
|
|
|
|
|
uint32_t discardValue = arc4random_uniform(101);
|
|
|
|
|
if (discardValue < kOWSAnalytics_DiscardFrequency) {
|
|
|
|
|
DDLogVerbose(@"Discarding event: %@", eventName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifndef NO_SIGNAL_ANALYTICS
|
|
|
|
|
dispatch_async(self.serialQueue, ^{
|
|
|
|
|
// Add super properties.
|
|
|
|
|
NSMutableDictionary *eventProperties = (properties ? [properties mutableCopy] : [NSMutableDictionary new]);
|
|
|
|
|
[eventProperties addEntriesFromDictionary:self.eventSuperProperties];
|
|
|
|
|
|
|
|
|
|
[[self sharedInstance] logEvent:description severity:severity parameters:parameters location:location];
|
|
|
|
|
NSDictionary *eventDictionary = [eventProperties copy];
|
|
|
|
|
OWSAssert(eventDictionary);
|
|
|
|
|
NSString *eventKey = [NSUUID UUID].UUIDString;
|
|
|
|
|
DDLogDebug(@"%@ enqueuing event: %@", self.tag, eventKey);
|
|
|
|
|
|
|
|
|
|
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
|
|
|
|
const int kMaxQueuedEvents = 5000;
|
|
|
|
|
if ([transaction numberOfKeysInCollection:kOWSAnalytics_EventsCollection] > kMaxQueuedEvents) {
|
|
|
|
|
DDLogError(@"%@ Event queue overflow.", self.tag);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)logEvent:(NSString *)description
|
|
|
|
|
[transaction setObject:eventDictionary forKey:eventKey inCollection:kOWSAnalytics_EventsCollection];
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
[self tryToSyncEvents];
|
|
|
|
|
});
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (void)logEvent:(NSString *)eventName
|
|
|
|
|
severity:(OWSAnalyticsSeverity)severity
|
|
|
|
|
parameters:(NSDictionary *)parameters
|
|
|
|
|
parameters:(nullable NSDictionary *)parameters
|
|
|
|
|
location:(const char *)location
|
|
|
|
|
line:(int)line
|
|
|
|
|
{
|
|
|
|
|
[[self sharedInstance] logEvent:eventName severity:severity parameters:parameters location:location line:line];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)logEvent:(NSString *)eventName
|
|
|
|
|
severity:(OWSAnalyticsSeverity)severity
|
|
|
|
|
parameters:(nullable NSDictionary *)parameters
|
|
|
|
|
location:(const char *)location
|
|
|
|
|
line:(int)line
|
|
|
|
|
{
|
|
|
|
|
DDLogFlag logFlag;
|
|
|
|
|
BOOL async = YES;
|
|
|
|
|
switch (severity) {
|
|
|
|
@ -64,13 +301,81 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log the event.
|
|
|
|
|
NSString *logString = [NSString stringWithFormat:@"%s:%d %@", location, line, eventName];
|
|
|
|
|
if (!parameters) {
|
|
|
|
|
LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", description);
|
|
|
|
|
LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", logString);
|
|
|
|
|
} else {
|
|
|
|
|
LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@ %@", description, parameters);
|
|
|
|
|
LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@ %@", logString, parameters);
|
|
|
|
|
}
|
|
|
|
|
if (!async) {
|
|
|
|
|
[DDLog flushLog];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSMutableDictionary *eventProperties = (parameters ? [parameters mutableCopy] : [NSMutableDictionary new]);
|
|
|
|
|
eventProperties[@"event_location"] = [NSString stringWithFormat:@"%s:%d", location, line];
|
|
|
|
|
[self addEvent:eventName properties:eventProperties];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do nothing. We don't yet serialize or transmit analytics events.
|
|
|
|
|
#pragma mark - Logging
|
|
|
|
|
|
|
|
|
|
+ (void)appLaunchDidBegin
|
|
|
|
|
{
|
|
|
|
|
[self.sharedInstance appLaunchDidBegin];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)appLaunchDidBegin
|
|
|
|
|
{
|
|
|
|
|
OWSProdInfo(@"app_launch");
|
|
|
|
|
|
|
|
|
|
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
|
NSNumber *oldLaunchCount =
|
|
|
|
|
[transaction objectForKey:kOWSAnalytics_KeyLaunchCount inCollection:kOWSAnalytics_Collection];
|
|
|
|
|
NSNumber *newLaunchCount = @(oldLaunchCount.longValue + 1);
|
|
|
|
|
self.launchCount = newLaunchCount;
|
|
|
|
|
|
|
|
|
|
NSNumber *oldLaunchCompleteCount =
|
|
|
|
|
[transaction objectForKey:kOWSAnalytics_KeyLaunchCompleteCount inCollection:kOWSAnalytics_Collection];
|
|
|
|
|
self.launchCompleteCount = @(oldLaunchCompleteCount.longValue);
|
|
|
|
|
}];
|
|
|
|
|
[TSStorageManager.sharedManager.newDatabaseConnection
|
|
|
|
|
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
|
|
|
[transaction setObject:self.launchCount
|
|
|
|
|
forKey:kOWSAnalytics_KeyLaunchCount
|
|
|
|
|
inCollection:kOWSAnalytics_Collection];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (void)appLaunchDidComplete
|
|
|
|
|
{
|
|
|
|
|
[self.sharedInstance appLaunchDidComplete];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)appLaunchDidComplete
|
|
|
|
|
{
|
|
|
|
|
OWSProdInfo(@"app_launch_complete");
|
|
|
|
|
|
|
|
|
|
self.launchCompleteCount = @(self.launchCompleteCount.longValue + 1);
|
|
|
|
|
|
|
|
|
|
[TSStorageManager.sharedManager.newDatabaseConnection
|
|
|
|
|
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
|
|
|
[transaction setObject:self.launchCompleteCount
|
|
|
|
|
forKey:kOWSAnalytics_KeyLaunchCompleteCount
|
|
|
|
|
inCollection:kOWSAnalytics_Collection];
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Logging
|
|
|
|
|
|
|
|
|
|
+ (NSString *)tag
|
|
|
|
|
{
|
|
|
|
|
return [NSString stringWithFormat:@"[%@]", self.class];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSString *)tag
|
|
|
|
|
{
|
|
|
|
|
return self.class.tag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|
|
|
|
|