session-ios/SignalServiceKit/src/Util/OWSAnalytics.m

428 lines
14 KiB
Mathematica
Raw Normal View History

2017-01-10 22:41:40 +01:00
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
2017-01-10 22:41:40 +01:00
//
#import "OWSAnalytics.h"
#import "AppContext.h"
2018-07-27 20:40:58 +02:00
#import "Cryptography.h"
2017-12-15 19:03:03 +01:00
#import "OWSBackgroundTask.h"
#import "OWSPrimaryStorage.h"
2017-07-26 16:52:15 +02:00
#import "OWSQueues.h"
2017-12-19 03:42:50 +01:00
#import "YapDatabaseConnection+OWS.h"
2017-01-10 22:41:40 +01:00
#import <CocoaLumberjack/CocoaLumberjack.h>
#import <Reachability/Reachability.h>
2017-12-19 03:42:50 +01:00
#import <YapDatabase/YapDatabase.h>
2017-01-10 22:41:40 +01:00
NS_ASSUME_NONNULL_BEGIN
#ifdef DEBUG
// TODO: Disable analytics for debug builds.
//#define NO_SIGNAL_ANALYTICS
#endif
NSString *const kOWSAnalytics_EventsCollection = @"kOWSAnalytics_EventsCollection";
// Percentage of analytics events to discard. 0 <= x <= 100.
const int kOWSAnalytics_DiscardFrequency = 0;
NSString *NSStringForOWSAnalyticsSeverity(OWSAnalyticsSeverity severity)
{
switch (severity) {
case OWSAnalyticsSeverityInfo:
return @"Info";
case OWSAnalyticsSeverityError:
return @"Error";
case OWSAnalyticsSeverityCritical:
return @"Critical";
}
}
@interface OWSAnalytics ()
@property (nonatomic, readonly) Reachability *reachability;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (atomic) BOOL hasRequestInFlight;
@end
#pragma mark -
2017-01-10 22:41:40 +01:00
@implementation OWSAnalytics
+ (instancetype)sharedInstance
{
static OWSAnalytics *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] initDefault];
2017-01-10 22:41:40 +01:00
});
return instance;
}
// We lazy-create the analytics DB connection, so that we can handle
// errors that occur while initializing OWSPrimaryStorage.
+ (YapDatabaseConnection *)dbConnection
{
static YapDatabaseConnection *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
OWSAssert(primaryStorage);
// Use a newDatabaseConnection so as not to block reads in the launch path.
instance = primaryStorage.newDatabaseConnection;
});
return instance;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
_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:OWSApplicationDidBecomeActiveNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)reachabilityChanged
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
[self tryToSyncEvents];
}
- (void)applicationDidBecomeActive
2017-01-10 22:41:40 +01:00
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2017-01-10 22:41:40 +01:00
[self tryToSyncEvents];
2017-01-10 22:41:40 +01:00
}
- (void)tryToSyncEvents
{
dispatch_async(self.serialQueue, ^{
// Don't try to sync if:
//
// * There's no network available.
// * There's already a sync request in flight.
if (!self.reachability.isReachable) {
OWSLogVerbose(@"%@ Not reachable", self.logTag);
return;
}
if (self.hasRequestInFlight) {
return;
}
__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) {
[self sendEvent:firstEventDictionary eventKey:firstEventKey isCritical:NO];
}
});
}
- (void)sendEvent:(NSDictionary *)eventDictionary eventKey:(NSString *)eventKey isCritical:(BOOL)isCritical
{
OWSAssert(eventDictionary);
OWSAssert(eventKey);
2017-07-26 16:52:15 +02:00
AssertOnDispatchQueue(self.serialQueue);
if (isCritical) {
[self submitEvent:eventDictionary
eventKey:eventKey
success:^{
OWSLogDebug(@"%@ sendEvent[critical] succeeded: %@", self.logTag, eventKey);
}
failure:^{
OWSLogError(@"%@ sendEvent[critical] failed: %@", self.logTag, eventKey);
}];
} else {
self.hasRequestInFlight = YES;
2017-07-26 17:58:41 +02:00
__block BOOL isComplete = NO;
[self submitEvent:eventDictionary
eventKey:eventKey
success:^{
2017-07-26 16:52:15 +02:00
if (isComplete) {
return;
}
isComplete = YES;
OWSLogDebug(@"%@ sendEvent succeeded: %@", self.logTag, eventKey);
dispatch_async(self.serialQueue, ^{
self.hasRequestInFlight = NO;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
// Remove from queue.
[transaction removeObjectForKey:eventKey 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];
});
});
}
failure:^{
2017-07-26 16:52:15 +02:00
if (isComplete) {
return;
}
isComplete = YES;
OWSLogError(@"%@ sendEvent failed: %@", self.logTag, eventKey);
dispatch_async(self.serialQueue, ^{
self.hasRequestInFlight = NO;
// 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];
});
});
}];
}
}
- (void)submitEvent:(NSDictionary *)eventDictionary
eventKey:(NSString *)eventKey
2017-12-01 01:57:56 +01:00
success:(void (^_Nonnull)(void))successBlock
failure:(void (^_Nonnull)(void))failureBlock
{
OWSAssert(eventDictionary);
OWSAssert(eventKey);
2017-07-26 16:52:15 +02:00
AssertOnDispatchQueue(self.serialQueue);
2017-07-24 23:18:15 +02:00
OWSLogDebug(@"%@ submitting: %@", self.logTag, eventKey);
2017-12-15 19:03:03 +01:00
__block OWSBackgroundTask *backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__
completionBlock:^(BackgroundTaskState backgroundTaskState) {
if (backgroundTaskState == BackgroundTaskState_Success) {
successBlock();
} else {
failureBlock();
}
}];
// Until we integrate with an analytics platform, behave as though all event delivery succeeds.
dispatch_async(self.serialQueue, ^{
2017-12-15 19:03:03 +01:00
backgroundTask = nil;
});
}
- (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;
}
- (NSString *)operatingSystemVersionString
{
static NSString *result = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSOperatingSystemVersion operatingSystemVersion = [[NSProcessInfo processInfo] operatingSystemVersion];
2018-07-18 03:08:53 +02:00
result = [NSString stringWithFormat:@"%lu.%lu.%lu",
(unsigned long)operatingSystemVersion.majorVersion,
(unsigned long)operatingSystemVersion.minorVersion,
(unsigned long)operatingSystemVersion.patchVersion];
});
return result;
}
- (NSDictionary<NSString *, id> *)eventSuperProperties
{
NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary new];
result[@"app_version"] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
result[@"platform"] = @"ios";
result[@"ios_version"] = self.operatingSystemVersionString;
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 severity:(OWSAnalyticsSeverity)severity properties:(NSDictionary *)properties
{
OWSAssert(eventName.length > 0);
OWSAssert(properties);
#ifndef NO_SIGNAL_ANALYTICS
BOOL isError = severity == OWSAnalyticsSeverityError;
BOOL isCritical = severity == OWSAnalyticsSeverityCritical;
uint32_t discardValue = arc4random_uniform(101);
if (!isError && !isCritical && discardValue < kOWSAnalytics_DiscardFrequency) {
OWSLogVerbose(@"Discarding event: %@", eventName);
return;
}
2017-12-01 01:57:56 +01:00
void (^addEvent)(void) = ^{
// Add super properties.
NSMutableDictionary *eventProperties = (properties ? [properties mutableCopy] : [NSMutableDictionary new]);
[eventProperties addEntriesFromDictionary:self.eventSuperProperties];
NSDictionary *eventDictionary = [eventProperties copy];
OWSAssert(eventDictionary);
NSString *eventKey = [NSUUID UUID].UUIDString;
OWSLogDebug(@"%@ enqueuing event: %@", self.logTag, eventKey);
if (isCritical) {
// Critical events should not be serialized or enqueued - they should be submitted immediately.
[self sendEvent:eventDictionary eventKey:eventKey isCritical:YES];
} else {
// Add to queue.
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
const int kMaxQueuedEvents = 5000;
if ([transaction numberOfKeysInCollection:kOWSAnalytics_EventsCollection] > kMaxQueuedEvents) {
OWSLogError(@"%@ Event queue overflow.", self.logTag);
return;
}
[transaction setObject:eventDictionary forKey:eventKey inCollection:kOWSAnalytics_EventsCollection];
}];
[self tryToSyncEvents];
}
};
2018-06-20 22:39:53 +02:00
if ([self shouldReportAsync:severity]) {
dispatch_async(self.serialQueue, addEvent);
} else {
dispatch_sync(self.serialQueue, addEvent);
}
#endif
}
+ (void)logEvent:(NSString *)eventName
2017-01-10 22:41:40 +01:00
severity:(OWSAnalyticsSeverity)severity
parameters:(nullable NSDictionary *)parameters
2017-01-10 22:41:40 +01:00
location:(const char *)location
line:(int)line
2017-01-10 22:41:40 +01:00
{
[[self sharedInstance] logEvent:eventName severity:severity parameters:parameters location:location line:line];
}
2017-01-10 22:41:40 +01:00
- (void)logEvent:(NSString *)eventName
severity:(OWSAnalyticsSeverity)severity
parameters:(nullable NSDictionary *)parameters
location:(const char *)location
line:(int)line
{
2017-01-10 22:41:40 +01:00
DDLogFlag logFlag;
switch (severity) {
case OWSAnalyticsSeverityInfo:
logFlag = DDLogFlagInfo;
break;
case OWSAnalyticsSeverityError:
logFlag = DDLogFlagError;
break;
case OWSAnalyticsSeverityCritical:
logFlag = DDLogFlagError;
break;
default:
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"Unknown severity.");
2017-01-10 22:41:40 +01:00
logFlag = DDLogFlagDebug;
break;
}
// Log the event.
NSString *logString = [NSString stringWithFormat:@"%s:%d %@", location, line, eventName];
2017-01-10 22:41:40 +01:00
if (!parameters) {
2018-06-20 22:39:53 +02:00
LOG_MAYBE([self shouldReportAsync:severity], LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", logString);
2017-01-10 22:41:40 +01:00
} else {
2018-06-20 22:39:53 +02:00
LOG_MAYBE([self shouldReportAsync:severity],
LOG_LEVEL_DEF,
logFlag,
0,
nil,
location,
@"%@ %@",
logString,
parameters);
}
2018-06-20 22:39:53 +02:00
if (![self shouldReportAsync:severity]) {
[DDLog flushLog];
2017-01-10 22:41:40 +01:00
}
NSMutableDictionary *eventProperties = (parameters ? [parameters mutableCopy] : [NSMutableDictionary new]);
eventProperties[@"event_location"] = [NSString stringWithFormat:@"%s:%d", location, line];
[self addEvent:eventName severity:severity properties:eventProperties];
}
2018-06-20 22:39:53 +02:00
- (BOOL)shouldReportAsync:(OWSAnalyticsSeverity)severity
{
return severity != OWSAnalyticsSeverityCritical;
}
#pragma mark - Logging
+ (void)appLaunchDidBegin
{
[self.sharedInstance appLaunchDidBegin];
}
- (void)appLaunchDidBegin
{
OWSProdInfo([OWSAnalyticsEvents appLaunch]);
}
2017-01-10 22:41:40 +01:00
@end
NS_ASSUME_NONNULL_END