Compare commits

..

2 Commits

Author SHA1 Message Date
Matthew Chen
8f2a48ec99 Instrument errors in message manager.
// FREEBIE
2017-07-21 14:39:41 -04:00
Matthew Chen
d857730601 Update analytics macros.
// FREEBIE
2017-07-21 11:50:19 -04:00
15 changed files with 622 additions and 151 deletions

View File

@ -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
@ -132,8 +134,9 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: f721ae4d5854bce60934f9fb9b0b28e8e68913cb
Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b
ProtocolBuffers: d509225eb2ea43d9582a59e94348fcf86e2abd65
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 1865333198217411f35327e8da61b43de79b635b
SignalServiceKit: 2ad8d86da055e24ac3ea0354ec1d4b13251af28f
SignalServiceKit: 2b6e0587fc6b4754e053b840083ed4ca9f32459d
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559
TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c

View File

@ -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 */,

View File

@ -1,27 +1,3 @@
# SignalServiceKit has Moved
Per https://github.com/WhisperSystems/Signal-iOS/pull/2341 we've moved
the SignalServiceKit codebase into the primary Signal-iOS repository at
https://github.com/WhisperSystems/Signal-iOS. As such, this repository
will no longer be updated.
Don't worry - we will continue to make updates to SignalServiceKit, and
you can continue to use it in your projects as before. The only
difference is where the code lives.
If you are using Cocoapods, staying up to date is as simple as modifying
a line in your Podfile from this:
```
- pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/SignalServiceKit.git'
```
To this:
```
+ pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/Signal-iOS.git'
```
# SignalServiceKit
SignalServiceKit is an Objective-C library for communicating with the Signal

View File

@ -17,37 +17,15 @@ An Objective-C library for communicating with the Signal messaging service.
s.homepage = "https://github.com/WhisperSystems/SignalServiceKit"
s.license = 'GPLv3'
s.author = { "Whisper Systems" => "ios@whispersystems.com" }
s.author = { "Frederic Jacobs" => "github@fredericjacobs.com" }
s.source = { :git => "https://github.com/WhisperSystems/SignalServiceKit.git", :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/WhipserSystems'
deprecation_message = <<EOS
installing SignalServiceKit via the Signal-iOS repository.
To get future updates, point your Podfile at the new location. Simply change this:
pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/SignalServiceKit.git'
To this:
pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/Signal-iOS.git'
Sorry for the disruption!
EOS
s.deprecated_in_favor_of = deprecation_message
s.social_media_url = 'https://twitter.com/FredericJacobs'
s.platform = :ios, '8.0'
#s.ios.deployment_target = '8.0'
#s.osx.deployment_target = '10.9'
s.requires_arc = true
# By not including any actual files, upgrading users will see
# that they need to point upgrades to the new source at
# https://github.com/WhisperSystems/Signal-iOS
# Details in README.md
s.source_files = 'README.md'
s.source_files = 'src/**/*.{h,m,mm}'
s.resources = ['src/Security/PinningCertificate/textsecure.cer',
'src/Security/PinningCertificate/GIAG2.crt']
@ -64,4 +42,5 @@ EOS
s.dependency 'libPhoneNumber-iOS'
s.dependency 'SAMKeychain'
s.dependency 'TwistedOakCollapsingFutures'
s.dependency 'Reachability'
end

View File

@ -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];
@ -387,10 +392,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];

View File

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

View File

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

View File

@ -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",

View File

@ -42,6 +42,23 @@
NS_ASSUME_NONNULL_BEGIN
#define kOWSProdAssertParameterEnvelopeIsLegacy @"envelope_is_legacy"
#define kOWSProdAssertParameterEnvelopeDescription @"envelope_description"
#define kOWSProdAssertParameterEnvelopeEncryptedLength @"encrypted_length"
#define AnalyticsParametersFromEnvelope(__envelope) \
^{ \
NSData *__encryptedData = __envelope.hasContent ? __envelope.content : __envelope.legacyMessage; \
return (@{ \
kOWSProdAssertParameterEnvelopeIsLegacy : @(__envelope.hasLegacyMessage), \
kOWSProdAssertParameterEnvelopeDescription : [self descriptionForEnvelopeType:__envelope], \
kOWSProdAssertParameterEnvelopeEncryptedLength : @(__encryptedData.length), \
}); \
}
#define OWSProdErrorWEnvelope(__analyticsEventName, __envelope) \
OWSProdErrorWParams(__analyticsEventName, AnalyticsParametersFromEnvelope(__envelope))
@interface TSMessagesManager ()
@property (nonatomic, readonly) id<OWSCallMessageHandler> callMessageHandler;
@ -135,39 +152,37 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Debugging
- (NSString *)descriptionForEnvelopeType:(OWSSignalServiceProtosEnvelope *)envelope
{
OWSAssert(envelope != nil);
switch (envelope.type) {
case OWSSignalServiceProtosEnvelopeTypeReceipt:
return @"DeliveryReceipt";
case OWSSignalServiceProtosEnvelopeTypeUnknown:
// Shouldn't happen
OWSProdFail(@"message_manager_error_envelope_type_unknown");
return @"Unknown";
case OWSSignalServiceProtosEnvelopeTypeCiphertext:
return @"SignalEncryptedMessage";
case OWSSignalServiceProtosEnvelopeTypeKeyExchange:
// Unsupported
OWSProdFail(@"message_manager_error_envelope_type_key_exchange");
return @"KeyExchange";
case OWSSignalServiceProtosEnvelopeTypePrekeyBundle:
return @"PreKeyEncryptedMessage";
default:
// Shouldn't happen
OWSProdFail(@"message_manager_error_envelope_type_other");
return @"Other";
}
}
- (NSString *)descriptionForEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
{
OWSAssert(envelope != nil);
NSString *envelopeType;
switch (envelope.type) {
case OWSSignalServiceProtosEnvelopeTypeReceipt:
envelopeType = @"DeliveryReceipt";
break;
case OWSSignalServiceProtosEnvelopeTypeUnknown:
// Shouldn't happen
OWSAssert(NO);
envelopeType = @"Unknown";
break;
case OWSSignalServiceProtosEnvelopeTypeCiphertext:
envelopeType = @"SignalEncryptedMessage";
break;
case OWSSignalServiceProtosEnvelopeTypeKeyExchange:
// Unsupported
OWSAssert(NO);
envelopeType = @"KeyExchange";
break;
case OWSSignalServiceProtosEnvelopeTypePrekeyBundle:
envelopeType = @"PreKeyEncryptedMessage";
break;
default:
// Shouldn't happen
OWSAssert(NO);
envelopeType = @"Other";
break;
}
return [NSString stringWithFormat:@"<Envelope type: %@, source: %@.%d, timestamp: %llu content.length: %lu />",
envelopeType,
[self descriptionForEnvelopeType:envelope],
envelope.source,
(unsigned int)envelope.sourceDevice,
envelope.timestamp,
@ -189,7 +204,9 @@ NS_ASSUME_NONNULL_BEGIN
} else if (content.hasNullMessage) {
return [NSString stringWithFormat:@"<NullMessage: %@ />", content.nullMessage];
} else {
OWSAssert(NO);
// Don't fire an analytics event; if we ever add a new content type, we'd generate a ton of
// analytics traffic.
OWSFail(@"Unknown content type.");
return @"UnknownContent";
}
}
@ -233,9 +250,11 @@ NS_ASSUME_NONNULL_BEGIN
[description appendString:@"ContactRequest"];
} else if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeGroups) {
[description appendString:@"GroupRequest"];
} else if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeBlocked) {
[description appendString:@"BlockedRequest"];
} else {
// Shouldn't happen
OWSAssert(NO);
OWSFail(@"Unknown sync message request type");
[description appendString:@"UnknownRequest"];
}
} else if (syncMessage.hasBlocked) {
@ -248,7 +267,7 @@ NS_ASSUME_NONNULL_BEGIN
[description appendString:verifiedString];
} else {
// Shouldn't happen
OWSAssert(NO);
OWSFail(@"Unknown sync message type");
[description appendString:@"Unknown"];
}
@ -295,6 +314,7 @@ NS_ASSUME_NONNULL_BEGIN
envelope.source,
(unsigned int)envelope.sourceDevice,
error);
OWSProdError(@"message_manager_error_could_not_handle_secure_message");
}
completion();
}];
@ -312,6 +332,7 @@ NS_ASSUME_NONNULL_BEGIN
envelope.source,
(unsigned int)envelope.sourceDevice,
error);
OWSProdError(@"message_manager_error_could_not_handle_prekey_bundle");
}
completion();
}];
@ -336,6 +357,7 @@ NS_ASSUME_NONNULL_BEGIN
}
} @catch (NSException *exception) {
DDLogError(@"Received an incorrectly formatted protocol buffer: %@", exception.debugDescription);
OWSProdFailWNSException(@"message_manager_error_invalid_protocol_message", exception);
}
completion();
@ -367,15 +389,18 @@ NS_ASSUME_NONNULL_BEGIN
NSData *encryptedData
= messageEnvelope.hasContent ? messageEnvelope.content : messageEnvelope.legacyMessage;
if (!encryptedData) {
DDLogError(@"Skipping message envelope which had no encrypted data.");
OWSProdFail(@"message_manager_error_message_envelope_has_no_content");
completion(nil);
return;
}
NSUInteger kMaxEncryptedDataLength = 250 * 1024;
if (encryptedData.length > kMaxEncryptedDataLength) {
DDLogError(@"Skipping message envelope with oversize encrypted data: %lu.",
(unsigned long)encryptedData.length);
OWSProdErrorWParams(@"message_manager_error_oversize_message", ^{
return (@{
@"message_size" : @(encryptedData.length),
});
});
completion(nil);
return;
}
@ -424,7 +449,7 @@ NS_ASSUME_NONNULL_BEGIN
// DEPRECATED - Remove after all clients have been upgraded.
NSData *encryptedData = preKeyEnvelope.hasContent ? preKeyEnvelope.content : preKeyEnvelope.legacyMessage;
if (!encryptedData) {
DDLogError(@"Skipping message envelope which had no encrypted data");
OWSProdFail(@"message_manager_error_prekey_bundle_envelope_has_no_content");
completion(nil);
return;
}
@ -816,7 +841,7 @@ NS_ASSUME_NONNULL_BEGIN
NSData *groupId = dataMessage.hasGroup ? dataMessage.group.id : nil;
if (!groupId) {
OWSAssert(groupId);
OWSFail(@"Group info request is missing group id.");
return;
}
@ -1009,24 +1034,30 @@ NS_ASSUME_NONNULL_BEGIN
__block TSErrorMessage *errorMessage;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if ([exception.name isEqualToString:NoSessionException]) {
OWSProdErrorWEnvelope(@"message_manager_error_no_session", envelope);
errorMessage = [TSErrorMessage missingSessionWithEnvelope:envelope withTransaction:transaction];
} else if ([exception.name isEqualToString:InvalidKeyException]) {
OWSProdErrorWEnvelope(@"message_manager_error_invalid_key", envelope);
errorMessage = [TSErrorMessage invalidKeyExceptionWithEnvelope:envelope withTransaction:transaction];
} else if ([exception.name isEqualToString:InvalidKeyIdException]) {
OWSProdErrorWEnvelope(@"message_manager_error_invalid_key_id", envelope);
errorMessage = [TSErrorMessage invalidKeyExceptionWithEnvelope:envelope withTransaction:transaction];
} else if ([exception.name isEqualToString:DuplicateMessageException]) {
// Duplicate messages are dismissed
return;
} else if ([exception.name isEqualToString:InvalidVersionException]) {
OWSProdErrorWEnvelope(@"message_manager_error_invalid_message_version", envelope);
errorMessage = [TSErrorMessage invalidVersionWithEnvelope:envelope withTransaction:transaction];
} else if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
// Should no longer get here, since we now record the new identity for incoming messages.
OWSProdErrorWEnvelope(@"message_manager_error_untrusted_identity_key_exception", envelope);
OWSFail(@"%@ Failed to trust identity on incoming message from: %@.%d",
self.tag,
envelope.source,
envelope.sourceDevice);
return;
} else {
OWSProdErrorWEnvelope(@"message_manager_error_corrupt_message", envelope);
errorMessage = [TSErrorMessage corruptedMessageWithEnvelope:envelope withTransaction:transaction];
}

View File

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

View File

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

14
src/Util/NSDate+OWS.h Executable file
View File

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

View File

@ -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,139 @@ 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 OWSAnalyticsLogEventWithParameters(severityLevel, frmt, params) \
[OWSAnalytics logEvent:frmt severity:severityLevel parameters:params location:__PRETTY_FUNCTION__];
#define kOWSProdAssertParameterDescription @"description"
#define kOWSProdAssertParameterNSErrorDomain @"nserror_domain"
#define kOWSProdAssertParameterNSErrorCode @"nserror_code"
#define kOWSProdAssertParameterNSErrorDescription @"nserror_description"
#define kOWSProdAssertParameterNSExceptionName @"nsexception_name"
#define kOWSProdAssertParameterNSExceptionReason @"nsexception_reason"
#define kOWSProdAssertParameterNSExceptionClassName @"nsexception_classname"
#define OWSAnalyticsDebug(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityDebug, frmt, ##__VA_ARGS__)
#define OWSAnalyticsDebugWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityDebug, description, params)
// 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 OWSAnalyticsInfo(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityInfo, frmt, ##__VA_ARGS__)
#define OWSAnalyticsInfoWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityInfo, description, params)
#define OWSProdAssertWParams(__value, __analyticsEventName, __parametersBlock) \
OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSAssert)
#define OWSAnalyticsWarn(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityWarn, frmt, ##__VA_ARGS__)
#define OWSAnalyticsWarnWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityWarn, description, params)
#define OWSProdCAssertWParams(__value, __analyticsEventName, __parametersBlock) \
OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSCAssert)
#define OWSAnalyticsError(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityError, frmt, ##__VA_ARGS__)
#define OWSAnalyticsErrorWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityError, description, params)
#define OWSProdAssert(__value, __analyticsEventName) OWSProdAssertWParams(__value, __analyticsEventName, nil)
#define OWSAnalyticsCritical(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityCritical, frmt, ##__VA_ARGS__)
#define OWSAnalyticsCriticalWithParameters(description, params) \
OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityCritical, description, params)
#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 AnalyticsParametersFromNSException(__exception) \
^{ \
return (@{ \
kOWSProdAssertParameterNSExceptionName : __exception.name, \
kOWSProdAssertParameterNSExceptionReason : __exception.reason, \
kOWSProdAssertParameterNSExceptionClassName : NSStringFromClass([__exception class]), \
}); \
}
#define OWSProdFailWNSError(__analyticsEventName, __nserror) \
OWSProdFailWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror))
#define OWSProdFailWNSException(__analyticsEventName, __exception) \
OWSProdFailWParams(__analyticsEventName, AnalyticsParametersFromNSException(__exception))
#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 OWSProdError(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityCritical, __analyticsEventName, nil)
#define OWSProdInfoWParams(__analyticsEventName, __parametersBlock) \
OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, __parametersBlock)
#define OWSProdInfo(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, nil)
#define OWSProdCFail(__analyticsEventName) OWSProdCFailWParams(__analyticsEventName, nil)
#define OWSProdErrorWNSError(__analyticsEventName, __nserror) \
OWSProdErrorWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror))
#define OWSProdErrorWNSException(__analyticsEventName, __exception) \
OWSProdErrorWParams(__analyticsEventName, AnalyticsParametersFromNSException(__exception))
NS_ASSUME_NONNULL_END

View File

@ -1,12 +1,53 @@
//
// OWSAnalytics.m
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import <CocoaLumberjack/CocoaLumberjack.h>
#import "OWSAnalytics.h"
#import "AppVersion.h"
#import "TSStorageManager.h"
#import <CocoaLumberjack/CocoaLumberjack.h>
#import <Reachability/Reachability.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];
[[self sharedInstance] logEvent:description severity:severity parameters:parameters location:location];
return [self initWithStorageManager:storageManager];
}
- (void)logEvent:(NSString *)description
severity:(OWSAnalyticsSeverity)severity
parameters:(NSDictionary *)parameters
location:(const char *)location
- (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];
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:(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

View File

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