Merge branch 'charlesmchen/analytics2'

pull/1/head
Matthew Chen 7 years ago
commit 3db5c777c2

@ -48,6 +48,7 @@ PODS:
- CocoaLumberjack
- libPhoneNumber-iOS
- Mantle
- Reachability
- SAMKeychain
- SocketRocket
- TwistedOakCollapsingFutures
@ -154,7 +155,7 @@ SPEC CHECKSUMS:
PureLayout: 4d550abe49a94f24c2808b9b95db9131685fe4cd
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 1865333198217411f35327e8da61b43de79b635b
SignalServiceKit: 0fa1aa668c13b51bca399ce970bc1f1d9297184b
SignalServiceKit: db63e60f9a9f851e9ae48214bee230be92bb7e57
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559
TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c

@ -159,6 +159,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
DDLogInfo(@"%@ application: didFinishLaunchingWithOptions completed.", self.tag);
[OWSAnalytics appLaunchDidBegin];
return YES;
}
@ -794,6 +796,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
[OWSAnalytics appLaunchDidComplete];
[AppVersion.instance appLaunchDidComplete];
[self ensureRootViewController];

@ -67,6 +67,7 @@
#import <MobileCoreServices/UTCoreTypes.h>
#import <SignalServiceKit/ContactsUpdater.h>
#import <SignalServiceKit/MimeTypeUtil.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/NSTimer+OWS.h>
#import <SignalServiceKit/OWSAddToContactsOfferMessage.h>
#import <SignalServiceKit/OWSAttachmentsProcessor.h>
@ -100,7 +101,7 @@ static const int JSQ_TOOLBAR_ICON_HEIGHT = 22;
static const int JSQ_TOOLBAR_ICON_WIDTH = 22;
static const int JSQ_IMAGE_INSET = 5;
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60;
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * kMinuteInterval;
NSString *const OWSMessagesViewControllerDidAppearNotification = @"OWSMessagesViewControllerDidAppear";

@ -5,6 +5,7 @@
#import "DebugUIDiskUsage.h"
#import "OWSTableViewController.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSOrphanedDataCleaner.h>
#import <SignalServiceKit/TSDatabaseView.h>
#import <SignalServiceKit/TSInteraction.h>
@ -85,11 +86,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)deleteOldMessages_3Months
{
NSTimeInterval kMinute = 60.f;
NSTimeInterval kHour = 60 * kMinute;
NSTimeInterval kDay = 24 * kHour;
NSTimeInterval kMonth = 30 * kDay;
[self deleteOldMessages:kMonth * 3];
[self deleteOldMessages:kMonthInterval * 3];
}
+ (void)deleteOldMessages:(NSTimeInterval)maxAgeSeconds

@ -4,6 +4,7 @@
#import "DebugLogger.h"
#import "OWSScrubbingLogFormatter.h"
#import <SignalServiceKit/NSDate+OWS.h>
#pragma mark Logging - Production logging wants us to write some logs to a file in case we need it for debugging.
@ -29,7 +30,7 @@
// Logging to file, because it's in the Cache folder, they are not uploaded in iTunes/iCloud backups.
self.fileLogger = [DDFileLogger new];
// 24 hour rolling.
self.fileLogger.rollingFrequency = 60 * 60 * 24;
self.fileLogger.rollingFrequency = kDayInterval;
// Keep last 3 days of logs - or last 3 logs (if logs rollover due to max file size).
self.fileLogger.logFileManager.maximumNumberOfLogFiles = 3;
// Raise the max file size per log file to 3 MB.

@ -6,6 +6,7 @@
#import "RegistrationViewController.h"
#import "Signal-Swift.h"
#import <ATAppUpdater/ATAppUpdater.h>
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/TSStorageManager.h>
NSString *const TSStorageManagerAppUpgradeNagCollection = @"TSStorageManagerAppUpgradeNagCollection";
@ -70,10 +71,7 @@ NSString *const TSStorageManagerAppUpgradeNagDate = @"TSStorageManagerAppUpgrade
NSDate *lastNagDate = [[TSStorageManager sharedManager] dateForKey:TSStorageManagerAppUpgradeNagDate
inCollection:TSStorageManagerAppUpgradeNagCollection];
const NSTimeInterval kMinute = 60.f;
const NSTimeInterval kHour = 60 * kMinute;
const NSTimeInterval kDay = 24 * kHour;
const NSTimeInterval kNagFrequency = kDay * 14;
const NSTimeInterval kNagFrequency = kDayInterval * 14;
BOOL canNag = (!lastNagDate || fabs(lastNagDate.timeIntervalSinceNow) > kNagFrequency);
if (!canNag) {
return;

@ -1,7 +1,9 @@
#import "DateUtil.h"
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#define ONE_DAY_TIME_INTERVAL (double)60 * 60 * 24
#define ONE_WEEK_TIME_INTERVAL (double)60 * 60 * 24 * 7
#import "DateUtil.h"
#import <SignalServiceKit/NSDate+OWS.h>
static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
@ -31,11 +33,11 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
}
+ (BOOL)dateIsOlderThanOneDay:(NSDate *)date {
return [[NSDate date] timeIntervalSinceDate:date] > ONE_DAY_TIME_INTERVAL;
return [[NSDate date] timeIntervalSinceDate:date] > kDayInterval;
}
+ (BOOL)dateIsOlderThanOneWeek:(NSDate *)date {
return [[NSDate date] timeIntervalSinceDate:date] > ONE_WEEK_TIME_INTERVAL;
return [[NSDate date] timeIntervalSinceDate:date] > kWeekInterval;
}
+ (BOOL)date:(NSDate *)date isEqualToDateIgnoringTime:(NSDate *)anotherDate {

@ -42,4 +42,5 @@ An Objective-C library for communicating with the Signal messaging service.
s.dependency 'libPhoneNumber-iOS'
s.dependency 'SAMKeychain'
s.dependency 'TwistedOakCollapsingFutures'
s.dependency 'Reachability'
end

@ -34,6 +34,7 @@ PODS:
- Mantle/extobjc (= 2.1.0)
- Mantle/extobjc (2.1.0)
- ProtocolBuffers (1.9.11)
- Reachability (3.2)
- SAMKeychain (1.5.2)
- SignalServiceKit (0.9.0):
- '25519'
@ -42,6 +43,7 @@ PODS:
- CocoaLumberjack
- libPhoneNumber-iOS
- Mantle
- Reachability
- SAMKeychain
- SocketRocket
- TwistedOakCollapsingFutures
@ -111,7 +113,7 @@ EXTERNAL SOURCES:
AxolotlKit:
:git: https://github.com/WhisperSystems/SignalProtocolKit.git
SignalServiceKit:
:path: "../../../SignalServiceKit.podspec"
:path: ../../../SignalServiceKit.podspec
SocketRocket:
:git: https://github.com/facebook/SocketRocket.git
@ -132,8 +134,9 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: f721ae4d5854bce60934f9fb9b0b28e8e68913cb
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
ProtocolBuffers: d509225eb2ea43d9582a59e94348fcf86e2abd65
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 1865333198217411f35327e8da61b43de79b635b
SignalServiceKit: 0fa1aa668c13b51bca399ce970bc1f1d9297184b
SignalServiceKit: db63e60f9a9f851e9ae48214bee230be92bb7e57
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559
TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
308D7DFA789594CEA62740D9 /* libPods-TSKitiOSTestAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DC1A83C39CBC09FB2405A3 /* libPods-TSKitiOSTestAppTests.a */; };
34D99C891F2250FF00D284D6 /* OWSAnalyticsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */; };
45046FE01D95A6130015EFF2 /* TSMessagesManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45046FDF1D95A6130015EFF2 /* TSMessagesManagerTest.m */; };
450E3C9A1D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 450E3C991D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m */; };
4516E3E81DD153CC00DC4206 /* TSGroupThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4516E3E71DD153CC00DC4206 /* TSGroupThreadTest.m */; };
@ -64,6 +65,7 @@
/* Begin PBXFileReference section */
1A50A62A8930EE2BC9B8AC11 /* Pods-TSKitiOSTestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.release.xcconfig"; sourceTree = "<group>"; };
31DFDA8F9523F5B15EA2376B /* Pods-TSKitiOSTestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.debug.xcconfig"; sourceTree = "<group>"; };
34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAnalyticsTests.m; sourceTree = "<group>"; };
36DA6C703F99948D553F4E3F /* Pods-TSKitiOSTestAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestAppTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestAppTests/Pods-TSKitiOSTestAppTests.debug.xcconfig"; sourceTree = "<group>"; };
45046FDF1D95A6130015EFF2 /* TSMessagesManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessagesManagerTest.m; path = ../../../tests/Messages/TSMessagesManagerTest.m; sourceTree = "<group>"; };
450E3C991D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDisappearingMessagesJobTest.m; path = ../../../tests/Messages/OWSDisappearingMessagesJobTest.m; sourceTree = "<group>"; };
@ -201,6 +203,7 @@
children = (
45458B731CC342B600A02153 /* CryptographyTests.m */,
45458B741CC342B600A02153 /* MessagePaddingTests.m */,
34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */,
);
name = Util;
path = ../../../tests/Util;
@ -563,6 +566,7 @@
45E741B61E5D14E800735842 /* OWSIncomingMessageFinderTest.m in Sources */,
452EE6D51D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m in Sources */,
450E3C9A1D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m in Sources */,
34D99C891F2250FF00D284D6 /* OWSAnalyticsTests.m in Sources */,
452EE6CF1D4A754C00E934BA /* TSThreadTest.m in Sources */,
45C6A09A1D2F029B007D8AC0 /* TSMessageTest.m in Sources */,
453E1FCF1DA8313100DDD7B7 /* OWSMessageSenderTest.m in Sources */,

@ -3,6 +3,7 @@
//
#import "TSPreKeyManager.h"
#import "NSDate+OWS.h"
#import "NSURLSessionDataTask+StatusCode.h"
#import "OWSIdentityManager.h"
#import "TSNetworkManager.h"
@ -13,17 +14,17 @@
// Time before deletion of signed prekeys (measured in seconds)
//
// Currently we retain signed prekeys for at least 7 days.
static const CGFloat kSignedPreKeysDeletionTime = 7 * 24 * 60 * 60;
static const NSTimeInterval kSignedPreKeysDeletionTime = 7 * kDayInterval;
// Time before rotation of signed prekeys (measured in seconds)
//
// Currently we rotate signed prekeys every 2 days (48 hours).
static const CGFloat kSignedPreKeyRotationTime = 2 * 24 * 60 * 60;
static const NSTimeInterval kSignedPreKeyRotationTime = 2 * kDayInterval;
// How often we check prekey state on app activation.
//
// Currently we check prekey state every 12 hours.
static const CGFloat kPreKeyCheckFrequencySeconds = 12 * 60 * 60;
static const NSTimeInterval kPreKeyCheckFrequencySeconds = 12 * kHourInterval;
// We generate 100 one-time prekeys at a time. We should replenish
// whenever ~2/3 of them have been consumed.
@ -40,7 +41,7 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5;
// before the message sending is disabled.
//
// Current value is 10 days (240 hours).
static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60 * 60;
static const NSTimeInterval kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * kDayInterval;
#pragma mark -
@ -180,7 +181,11 @@ static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60
[TSPreKeyManager clearPreKeyUpdateFailureCount];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
OWSAnalyticsError(@"Prekey update failed (%@): %@", description, error);
if (modeCopy == RefreshPreKeysMode_SignedAndOneTime) {
OWSProdErrorWNSError(@"error_prekeys_update_failed_signed_and_onetime", error);
} else {
OWSProdErrorWNSError(@"error_prekeys_update_failed_just_signed", error);
}
// Mark the prekeys as _NOT_ checked on failure.
[self markPreKeysAsNotChecked];
@ -388,10 +393,12 @@ static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60
}
}
OWSAnalyticsInfo(@"%@ Deleting old signed prekey: %@, wasAcceptedByService: %d",
self.tag,
[dateFormatter stringFromDate:signedPrekey.generatedAt],
signedPrekey.wasAcceptedByService);
OWSProdInfoWParams(@"prekeys_deleted_old_signed_prekey", ^{
return (@{
@"generated" : [dateFormatter stringFromDate:signedPrekey.generatedAt],
@"accepted" : @(signedPrekey.wasAcceptedByService),
});
});
oldSignedPreKeyCount--;
[storageManager removeSignedPreKey:signedPrekey.Id];

@ -1,11 +1,14 @@
// Created by Michael Kirk on 9/23/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSDisappearingMessagesConfiguration.h"
#import "NSDate+OWS.h"
NS_ASSUME_NONNULL_BEGIN
const uint32_t OWSDisappearingMessagesConfigurationDefaultExpirationDuration = 60 * 60 * 24; // 1 day.
// 1 day.
const uint32_t OWSDisappearingMessagesConfigurationDefaultExpirationDuration = kDayInterval;
@interface OWSDisappearingMessagesConfiguration ()

@ -4,6 +4,7 @@
#import "OWSDisappearingMessagesJob.h"
#import "ContactsManagerProtocol.h"
#import "NSDate+OWS.h"
#import "NSDate+millisecondTimeStamp.h"
#import "NSTimer+OWS.h"
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
@ -300,7 +301,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSTimeInterval)maxDelaySeconds
{
// Don't run less often than once per N minutes.
return 5 * 60.f;
return 5 * kMinuteInterval;
}
// Waits the maximum amount of time to run again.

@ -836,7 +836,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
AssertIsOnSendingQueue();
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
OWSAnalyticsError(@"Message send failed due to prekey update failures");
OWSProdError(@"message_send_error_failed_due_to_prekey_update_failures");
// Retry prekey update every time user tries to send a message while app
// is disabled due to prekey update failures.
@ -878,7 +878,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// We expect it to happen whenever Bob reinstalls, and Alice messages Bob before
// she can pull down his latest identity.
// If it's happening a lot, we should rethink our profile fetching strategy.
OWSAnalyticsInfo(@"Message send failed due to untrusted key.");
OWSProdInfo(@"message_send_error_failed_due_to_untrusted_key");
NSString *localizedErrorDescriptionFormat
= NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY",

@ -3,6 +3,7 @@
//
#import "OWSOrphanedDataCleaner.h"
#import "NSDate+OWS.h"
#import "TSAttachmentStream.h"
#import "TSInteraction.h"
#import "TSMessage.h"
@ -134,7 +135,7 @@ NS_ASSUME_NONNULL_BEGIN
#ifdef SSK_BUILDING_FOR_TESTS
const NSTimeInterval kMinimumOrphanAge = 0.f;
#else
const NSTimeInterval kMinimumOrphanAge = 15 * 60.f;
const NSTimeInterval kMinimumOrphanAge = 15 * kMinuteInterval;
#endif
if (!shouldCleanup) {

@ -117,14 +117,14 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
// The best we can try to do is to discard the current database
// and behave like a clean install.
OWSAnalyticsCritical(@"Could not load database");
OWSProdError(@"storage_error_could_not_load_database");
// Try to reset app by deleting database.
// Disabled resetting storage until we have better data on why this happens.
// [self resetSignalStorage];
if (![self tryToLoadDatabase]) {
OWSAnalyticsCritical(@"Could not load database (second attempt)");
OWSProdError(@"storage_error_could_not_load_database_second_attempt");
[NSException raise:TSStorageManagerExceptionNameNoDatabase format:@"Failed to initialize database."];
}
@ -353,8 +353,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
BOOL shouldHavePassword = [NSFileManager.defaultManager fileExistsAtPath:[self dbPath]];
if (shouldHavePassword) {
OWSAnalyticsCriticalWithParameters(@"Could not retrieve database password from keychain",
@{ @"ErrorCode" : @(keyFetchError.code) });
OWSProdErrorWNSError(@"storage_error_could_not_load_database_second_attempt", keyFetchError);
}
// Try to reset app by deleting database.

@ -0,0 +1,14 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// These NSTimeInterval constants provide simplified durations for readability.
#define kMinuteInterval 60
#define kHourInterval (60 * kMinuteInterval)
#define kDayInterval (24 * kHourInterval)
#define kWeekInterval (7 * kDayInterval)
#define kMonthInterval (30 * kDayInterval)
NS_ASSUME_NONNULL_END

@ -1,9 +1,10 @@
//
// OWSAnalytics.h
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// TODO: We probably don't need all of these levels.
typedef NS_ENUM(NSUInteger, OWSAnalyticsSeverity) {
OWSAnalyticsSeverityDebug = 0,
OWSAnalyticsSeverityInfo = 1,
@ -32,41 +33,121 @@ typedef NS_ENUM(NSUInteger, OWSAnalyticsSeverity) {
@interface OWSAnalytics : NSObject
// description: A non-empty string without any leading whitespace.
// This should conform to our analytics event naming conventions.
// "category_event_name", e.g. "database_error_no_database_file_found".
// parameters: Optional.
// If non-nil, the keys should all be non-empty NSStrings.
// Values should be NSStrings or NSNumbers.
+ (void)logEvent:(NSString *)description
+ (void)logEvent:(NSString *)eventName
severity:(OWSAnalyticsSeverity)severity
parameters:(NSDictionary *)parameters
location:(const char *)location;
parameters:(nullable NSDictionary *)parameters
location:(const char *)location
line:(int)line;
+ (void)appLaunchDidBegin;
+ (void)appLaunchDidComplete;
@end
#define OWSAnalyticsLogEvent(severityLevel, frmt, ...) \
[OWSAnalytics logEvent:[NSString stringWithFormat:frmt, ##__VA_ARGS__] \
severity:severityLevel \
parameters:nil \
location:__PRETTY_FUNCTION__];
typedef NSDictionary<NSString *, id> *_Nonnull (^OWSProdAssertParametersBlock)();
#define kOWSProdAssertParameterDescription @"description"
#define kOWSProdAssertParameterNSErrorDomain @"nserror_domain"
#define kOWSProdAssertParameterNSErrorCode @"nserror_code"
#define kOWSProdAssertParameterNSErrorDescription @"nserror_description"
// These methods should be used to assert errors for which we want to fire analytics events.
//
// In production, returns __Value, the assert value, so that we can handle this case.
// In debug builds, asserts.
//
// parametersBlock is of type OWSProdAssertParametersBlock.
// The "C" variants (e.g. OWSProdAssert() vs. OWSProdCAssert() should be used in free functions,
// where there is no self.
//
#define OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, __assertMacro) \
{ \
if (!(BOOL)(__value)) { \
NSDictionary<NSString *, id> *__eventParameters = (__parametersBlock ? __parametersBlock() : nil); \
[DDLog flushLog]; \
[OWSAnalytics logEvent:__analyticsEventName \
severity:OWSAnalyticsSeverityError \
parameters:__eventParameters \
location:__PRETTY_FUNCTION__ \
line:__LINE__]; \
} \
__assertMacro(__value); \
return (BOOL)(__value); \
}
#define OWSProdAssertWParams(__value, __analyticsEventName, __parametersBlock) \
OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSAssert)
#define OWSProdCAssertWParams(__value, __analyticsEventName, __parametersBlock) \
OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSCAssert)
#define OWSProdAssert(__value, __analyticsEventName) OWSProdAssertWParams(__value, __analyticsEventName, nil)
#define OWSProdCAssert(__value, __analyticsEventName) OWSProdCAssertWParams(__value, __analyticsEventName, nil)
#define OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, __failMacro) \
{ \
NSDictionary<NSString *, id> *__eventParameters \
= (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \
[OWSAnalytics logEvent:__analyticsEventName \
severity:OWSAnalyticsSeverityCritical \
parameters:__eventParameters \
location:__PRETTY_FUNCTION__ \
line:__LINE__]; \
__failMacro(__analyticsEventName); \
}
#define OWSProdFailWParams(__analyticsEventName, __parametersBlock) \
OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, OWSFail)
#define OWSProdCFailWParams(__analyticsEventName, __parametersBlock) \
OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, OWSCFail)
#define OWSProdFail(__analyticsEventName) OWSProdFailWParams(__analyticsEventName, nil)
#define OWSProdCFail(__analyticsEventName) OWSProdCFailWParams(__analyticsEventName, nil)
#define AnalyticsParametersFromNSError(__nserror) \
^{ \
return (@{ \
kOWSProdAssertParameterNSErrorDomain : __nserror.domain, \
kOWSProdAssertParameterNSErrorCode : @(__nserror.code), \
kOWSProdAssertParameterNSErrorDescription : __nserror.description, \
}); \
}
#define OWSProdFailWNSError(__analyticsEventName, __nserror) \
OWSProdFailWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror))
#define OWSProdEventWParams(__severityLevel, __analyticsEventName, __parametersBlock) \
{ \
NSDictionary<NSString *, id> *__eventParameters \
= (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \
[OWSAnalytics logEvent:__analyticsEventName \
severity:OWSAnalyticsSeverityCritical \
parameters:__eventParameters \
location:__PRETTY_FUNCTION__ \
line:__LINE__]; \
}
#define OWSProdErrorWParams(__analyticsEventName, __parametersBlock) \
OWSProdEventWParams(OWSAnalyticsSeverityCritical, __analyticsEventName, __parametersBlock)
#define OWSAnalyticsLogEventWithParameters(severityLevel, frmt, params) \
[OWSAnalytics logEvent:frmt severity:severityLevel parameters:params location:__PRETTY_FUNCTION__];
#define OWSProdError(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityCritical, __analyticsEventName, nil)
#define OWSAnalyticsDebug(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityDebug, frmt, ##__VA_ARGS__)
#define OWSAnalyticsDebugWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityDebug, description, params)
#define OWSProdInfoWParams(__analyticsEventName, __parametersBlock) \
OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, __parametersBlock)
#define OWSAnalyticsInfo(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityInfo, frmt, ##__VA_ARGS__)
#define OWSAnalyticsInfoWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityInfo, description, params)
#define OWSProdInfo(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, nil)
#define OWSAnalyticsWarn(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityWarn, frmt, ##__VA_ARGS__)
#define OWSAnalyticsWarnWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityWarn, description, params)
#define OWSProdCFail(__analyticsEventName) OWSProdCFailWParams(__analyticsEventName, nil)
#define OWSAnalyticsError(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityError, frmt, ##__VA_ARGS__)
#define OWSAnalyticsErrorWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityError, description, params)
#define OWSProdErrorWNSError(__analyticsEventName, __nserror) \
OWSProdErrorWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror))
#define OWSAnalyticsCritical(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityCritical, frmt, ##__VA_ARGS__)
#define OWSAnalyticsCriticalWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityCritical, description, params)
NS_ASSUME_NONNULL_END

@ -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 sharedInstance] logEvent:description severity:severity parameters:parameters location:location];
[self tryToSyncEvents];
}
- (void)logEvent:(NSString *)description
- (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];
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;
}
[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];
}
// Do nothing. We don't yet serialize or transmit analytics events.
NSMutableDictionary *eventProperties = (parameters ? [parameters mutableCopy] : [NSMutableDictionary new]);
eventProperties[@"event_location"] = [NSString stringWithFormat:@"%s:%d", location, line];
[self addEvent:eventName properties:eventProperties];
}
#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

@ -0,0 +1,49 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "NSData+Base64.h"
#import "OWSAnalytics.h"
#import <XCTest/XCTest.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSAnalyticsTests : XCTestCase
@end
@interface OWSAnalytics (Test)
+ (long)orderOfMagnitudeOf:(long)value;
@end
@implementation OWSAnalyticsTests
- (void)testOrderOfMagnitudeOf
{
XCTAssertEqual(0, [OWSAnalytics orderOfMagnitudeOf:-1]);
XCTAssertEqual(0, [OWSAnalytics orderOfMagnitudeOf:0]);
XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:1]);
XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:5]);
XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:9]);
XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:10]);
XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:11]);
XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:19]);
XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:99]);
XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:100]);
XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:303]);
XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:999]);
XCTAssertEqual(1000, [OWSAnalytics orderOfMagnitudeOf:1000]);
XCTAssertEqual(1000, [OWSAnalytics orderOfMagnitudeOf:3030]);
XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:10000]);
XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:30303]);
XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:99999]);
XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:100000]);
XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:303030]);
XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:999999]);
}
@end
NS_ASSUME_NONNULL_END
Loading…
Cancel
Save