Merge pull request #1917 from vector-im/e2e_telemetry_

Crypto: Add telemetry for events unable to decrypt (UTDs)
This commit is contained in:
manuroe
2018-06-29 12:05:16 +02:00
committed by GitHub
10 changed files with 359 additions and 36 deletions
+1
View File
@@ -6,6 +6,7 @@ Improvements:
* RoomVC: Add a re-request keys button on message unable to decrypt (#1879).
* Analytics: Move code from AppDelegate to a dedicated class: Analytics.
* Analytics: Track Matrix SDK stats (time to startup the app).
* Crypto: Add telemetry for events unable to decrypt (UTDs).
* Added the i18n localisation strings to the accessibility labels (#1842), thanks to @einMarco (PR#1906).
* Added titles to sound files ID3 tags.
+12
View File
@@ -64,6 +64,8 @@
3267EFB820E379FE00FF1CAA /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = 3267EFB420E379FD00FF1CAA /* Podfile */; };
3267EFB920E379FE00FF1CAA /* AUTHORS.rst in Resources */ = {isa = PBXBuildFile; fileRef = 3267EFB520E379FD00FF1CAA /* AUTHORS.rst */; };
3267EFBA20E379FE00FF1CAA /* README.rst in Resources */ = {isa = PBXBuildFile; fileRef = 3267EFB620E379FD00FF1CAA /* README.rst */; };
3267EFC020E4A3DD00FF1CAA /* DecryptionFailureTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 3267EFBE20E4A3DD00FF1CAA /* DecryptionFailureTracker.m */; };
3267EFC320E5055800FF1CAA /* DecryptionFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = 3267EFC220E5055800FF1CAA /* DecryptionFailure.m */; };
327382B51F276AD200356143 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 327382A81F276AD200356143 /* InfoPlist.strings */; };
327382B61F276AD200356143 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 327382AA1F276AD200356143 /* Localizable.strings */; };
327382B71F276AD200356143 /* Vector.strings in Resources */ = {isa = PBXBuildFile; fileRef = 327382AC1F276AD200356143 /* Vector.strings */; };
@@ -736,6 +738,10 @@
3267EFB420E379FD00FF1CAA /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = "<group>"; };
3267EFB520E379FD00FF1CAA /* AUTHORS.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = AUTHORS.rst; sourceTree = "<group>"; };
3267EFB620E379FD00FF1CAA /* README.rst */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.rst; sourceTree = "<group>"; };
3267EFBE20E4A3DD00FF1CAA /* DecryptionFailureTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DecryptionFailureTracker.m; path = Riot/Analytics/DecryptionFailureTracker.m; sourceTree = SOURCE_ROOT; };
3267EFBF20E4A3DD00FF1CAA /* DecryptionFailureTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DecryptionFailureTracker.h; path = Riot/Analytics/DecryptionFailureTracker.h; sourceTree = SOURCE_ROOT; };
3267EFC120E5055800FF1CAA /* DecryptionFailure.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DecryptionFailure.h; path = Riot/Analytics/DecryptionFailure.h; sourceTree = SOURCE_ROOT; };
3267EFC220E5055800FF1CAA /* DecryptionFailure.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = DecryptionFailure.m; path = Riot/Analytics/DecryptionFailure.m; sourceTree = SOURCE_ROOT; };
327382A91F276AD200356143 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = InfoPlist.strings; sourceTree = "<group>"; };
327382AB1F276AD200356143 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = Localizable.strings; sourceTree = "<group>"; };
327382AD1F276AD200356143 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = Vector.strings; sourceTree = "<group>"; };
@@ -1613,6 +1619,10 @@
children = (
3267EFB020E2A04100FF1CAA /* Analytics.h */,
3267EFB120E2A04100FF1CAA /* Analytics.m */,
3267EFBF20E4A3DD00FF1CAA /* DecryptionFailureTracker.h */,
3267EFBE20E4A3DD00FF1CAA /* DecryptionFailureTracker.m */,
3267EFC120E5055800FF1CAA /* DecryptionFailure.h */,
3267EFC220E5055800FF1CAA /* DecryptionFailure.m */,
);
name = Analytics;
path = "New Group";
@@ -3622,6 +3632,7 @@
F083BE2D1E7009ED00A9B29C /* ForgotPasswordInputsView.m in Sources */,
F083BE7E1E7009ED00A9B29C /* InviteRecentTableViewCell.m in Sources */,
F083BE3C1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentWithoutSenderInfoBubbleCell.m in Sources */,
3267EFC320E5055800FF1CAA /* DecryptionFailure.m in Sources */,
F083BE211E7009ED00A9B29C /* RoomSearchViewController.m in Sources */,
32185B311F20FA2B00752141 /* LanguagePickerViewController.m in Sources */,
F083BE3E1E7009ED00A9B29C /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.m in Sources */,
@@ -3686,6 +3697,7 @@
F083BE101E7009ED00A9B29C /* CountryPickerViewController.m in Sources */,
F083BE581E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */,
F083BDF61E7009ED00A9B29C /* Contact.m in Sources */,
3267EFC020E4A3DD00FF1CAA /* DecryptionFailureTracker.m in Sources */,
F083BE961E7009ED00A9B29C /* MessagesSearchResultAttachmentBubbleCell.m in Sources */,
F083BE391E7009ED00A9B29C /* RoomEncryptedDataBubbleCell.m in Sources */,
F083BDF01E7009ED00A9B29C /* UIViewController+RiotSearch.m in Sources */,
+2 -1
View File
@@ -17,11 +17,12 @@
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "DecryptionFailureTracker.h"
/**
`Analytics` sends analytics to an analytics tool.
*/
@interface Analytics : NSObject <MXAnalyticsDelegate>
@interface Analytics : NSObject <MXAnalyticsDelegate, MXDecryptionFailureDelegate>
/**
Returns the shared Analytics manager.
+29 -12
View File
@@ -22,8 +22,14 @@
// Then, there are 2 Piwik actions: "iOS.startup" and "iOS.stats" (these actions
// are namespaced by plaform to have a nice rendering on the Piwik website).
// Then, we use constants defined by the Matrix SDK as Piwik Names (ex:"mountData")
NSString *const kAnalyticsPiwikMetricsCategory = @"Metrics";
NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
NSString *const kAnalyticsMetricsCategory = @"Metrics";
NSString *const kAnalyticsMetricsActionPattern = @"iOS.%@";
// E2E telemetry is stored under a Piwik category called "E2E".
NSString *const kAnalyticsE2eCategory = @"E2E";
NSString *const kAnalyticsE2eDecryptionFailureAction = @"Decryption failure";
NSString *const kAnalyticsE2eDecryptionFailureReasonNoReason = @"no_reason";
@import PiwikTracker;
@@ -111,9 +117,9 @@ NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
- (void)trackLaunchScreenDisplayDuration:(NSTimeInterval)seconds
{
NSString *action = [NSString stringWithFormat:kAnalyticsPiwikMetricsActionPattern, kMXAnalyticsStartupCategory];
NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory];
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsPiwikMetricsCategory
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory
action:action
name:kMXAnalyticsStartupLaunchScreen
number:@(seconds * 1000)
@@ -124,9 +130,9 @@ NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
- (void)trackStartupStorePreloadDuration: (NSTimeInterval)seconds
{
NSString *action = [NSString stringWithFormat:kAnalyticsPiwikMetricsActionPattern, kMXAnalyticsStartupCategory];
NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory];
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsPiwikMetricsCategory
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory
action:action
name:kMXAnalyticsStartupStorePreload
number:@(seconds * 1000)
@@ -135,9 +141,9 @@ NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
- (void)trackStartupMountDataDuration: (NSTimeInterval)seconds
{
NSString *action = [NSString stringWithFormat:kAnalyticsPiwikMetricsActionPattern, kMXAnalyticsStartupCategory];
NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory];
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsPiwikMetricsCategory
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory
action:action
name:kMXAnalyticsStartupMountData
number:@(seconds * 1000)
@@ -146,9 +152,9 @@ NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
- (void)trackStartupSyncDuration: (NSTimeInterval)seconds isInitial: (BOOL)isInitial
{
NSString *action = [NSString stringWithFormat:kAnalyticsPiwikMetricsActionPattern, kMXAnalyticsStartupCategory];
NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStartupCategory];
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsPiwikMetricsCategory
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory
action:action
name:isInitial ? kMXAnalyticsStartupInititialSync : kMXAnalyticsStartupIncrementalSync
number:@(seconds * 1000)
@@ -157,13 +163,24 @@ NSString *const kAnalyticsPiwikMetricsActionPattern = @"iOS.%@";
- (void)trackRoomCount: (NSUInteger)roomCount
{
NSString *action = [NSString stringWithFormat:kAnalyticsPiwikMetricsActionPattern, kMXAnalyticsStatsCategory];
NSString *action = [NSString stringWithFormat:kAnalyticsMetricsActionPattern, kMXAnalyticsStatsCategory];
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsPiwikMetricsCategory
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsMetricsCategory
action:action
name:kMXAnalyticsStatsRooms
number:@(roomCount)
url:nil];
}
#pragma mark - MXDecryptionFailureDelegate
- (void)trackFailures:(NSUInteger)failuresCount
{
[[PiwikTracker shared] trackWithEventWithCategory:kAnalyticsE2eCategory
action:kAnalyticsE2eDecryptionFailureAction
name:kAnalyticsE2eDecryptionFailureReasonNoReason
number:@(failuresCount)
url:nil];
}
@end
+39
View File
@@ -0,0 +1,39 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
/**
`DecryptionFailure` represents a decryption failure.
*/
@interface DecryptionFailure : NSObject
/**
The id of the event that was unabled to decrypt.
*/
@property (nonatomic) NSString *failedEventId;
/**
The time the failure has been reported.
*/
@property (nonatomic, readonly) NSTimeInterval ts;
/**
Decryption failure reason.
*/
@property (nonatomic) NSString *reason;
@end
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "DecryptionFailure.h"
@implementation DecryptionFailure
- (instancetype)init
{
self = [super init];
if (self)
{
_ts = [NSDate date].timeIntervalSince1970;
}
return self;
}
@end
+69
View File
@@ -0,0 +1,69 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
@import MatrixSDK;
@protocol MXDecryptionFailureDelegate;
@interface DecryptionFailureTracker : NSObject
/**
Returns the shared tracker.
@return the shared tracker.
*/
+ (instancetype)sharedInstance;
/**
The delegate object to receive analytics events.
*/
@property (nonatomic) id<MXDecryptionFailureDelegate> delegate;
/**
Report an event unable to decrypt.
This error can be momentary. The DecryptionFailureTracker will check if it gets
fixed. Else, it will generate a failure (@see `trackFailures`).
@param event the event.
@param roomState the room state when the event was received.
@param userId my user id.
*/
- (void)reportUnableToDecryptErrorForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState myUser:(NSString*)userId;
/**
Flush current data.
*/
- (void)dispatch;
@end
/**
The `MXDecryptionFailureDelegate` protocol receives some stats computed by
`DecryptionFailureTracker`.
*/
@protocol MXDecryptionFailureDelegate <NSObject>
/**
Stats for decryption failures.
@param failuresCount the number of decryption failures.
*/
- (void)trackFailures:(NSUInteger)failuresCount;
@end
+143
View File
@@ -0,0 +1,143 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "DecryptionFailureTracker.h"
#import "DecryptionFailure.h"
// Call `checkFailures` every `CHECK_INTERVAL`
#define CHECK_INTERVAL 5
// Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting
// and reporting them as failures
#define GRACE_PERIOD 60
@interface DecryptionFailureTracker()
{
// Reported failures
// Every `CHECK_INTERVAL`, this list is checked for failures that happened
// more than`GRACE_PERIOD` ago. Those that did are reported to the delegate.
NSMutableDictionary<NSString* /* eventId */, DecryptionFailure*> *reportedFailures;
// Event ids of failures that were tracked previously
NSMutableSet<NSString*> *trackedEvents;
// Timer for periodic check
NSTimer *checkFailuresTimer;
}
@end
@implementation DecryptionFailureTracker
+ (instancetype)sharedInstance
{
static DecryptionFailureTracker *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[DecryptionFailureTracker alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (self)
{
reportedFailures = [NSMutableDictionary dictionary];
trackedEvents = [NSMutableSet set];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil];
checkFailuresTimer = [NSTimer scheduledTimerWithTimeInterval:CHECK_INTERVAL
target:self
selector:@selector(checkFailures)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState myUser:(NSString *)userId
{
if (reportedFailures[event.eventId] || [trackedEvents containsObject:event.eventId])
{
return;
}
// Filter out "expected" UTDs
// We cannot decrypt messages sent before the user joined the room
MXRoomMember *myUser = [roomState memberWithUserId:userId];
if (!myUser || myUser.membership != MXMembershipJoin)
{
return;
}
DecryptionFailure *decryptionFailure = [[DecryptionFailure alloc] init];
decryptionFailure.failedEventId = event.eventId;
// TODO: Need to sync with all platforms
// decryptionFailure.reason =;
reportedFailures[event.eventId] = decryptionFailure;
}
- (void)dispatch
{
[self checkFailures];
}
#pragma mark - Private methods
/**
Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be
tracked.
*/
- (void)checkFailures
{
NSTimeInterval tsNow = [NSDate date].timeIntervalSince1970;
NSMutableArray *failuresToTrack = [NSMutableArray array];
for (DecryptionFailure *reportedFailure in reportedFailures.allValues)
{
if (reportedFailure.ts < tsNow - GRACE_PERIOD)
{
[failuresToTrack addObject:reportedFailure];
[reportedFailures removeObjectForKey:reportedFailure.failedEventId];
[trackedEvents addObject:reportedFailure.failedEventId];
}
}
if (_delegate && failuresToTrack.count)
{
NSLog(@"[DecryptionFailureTracker] trackFailures: %@", @(failuresToTrack.count));
[_delegate trackFailures:failuresToTrack.count];
}
}
- (void)eventDidDecrypt:(NSNotification *)notif
{
// Could be an event in the reportedFailures, remove it
MXEvent *event = notif.object;
[reportedFailures removeObjectForKey:event.eventId];
}
@end
+2
View File
@@ -464,6 +464,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Configure our analytics. It will indeed start if the option is enabled
[MXSDKOptions sharedInstance].analyticsDelegate = [Analytics sharedInstance];
[DecryptionFailureTracker sharedInstance].delegate = [Analytics sharedInstance];
[[Analytics sharedInstance] start];
// Prepare Pushkit handling
@@ -558,6 +559,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
_isAppForeground = NO;
// Analytics: Force to send the pending actions
[[DecryptionFailureTracker sharedInstance] dispatch];
[[Analytics sharedInstance] dispatch];
}
+31 -23
View File
@@ -22,6 +22,7 @@
#import "WidgetManager.h"
#import "MXDecryptionResult.h"
#import "DecryptionFailureTracker.h"
#pragma mark - Constants definitions
@@ -117,34 +118,41 @@ NSString *const kEventFormatterOnReRequestKeysLinkActionSeparator = @"/";
NSAttributedString *attributedString = [super attributedStringFromEvent:event withRoomState:roomState error:error];
if (event.sentState == MXEventSentStateSent
&& [event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain]
&& event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode)
&& [event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain])
{
// Append to the displayed error an attibuted string with a tappable link
// so that the user can try to fix the UTC
NSMutableAttributedString *attributedStringWithRerequestMessage = [attributedString mutableCopy];
[attributedStringWithRerequestMessage appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]];
// Track e2e failures
dispatch_async(dispatch_get_main_queue(), ^{
[[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState myUser:mxSession.myUser.userId];
});
NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", kEventFormatterOnReRequestKeysLinkAction,
kEventFormatterOnReRequestKeysLinkActionSeparator,
event.eventId];
if (event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode)
{
// Append to the displayed error an attibuted string with a tappable link
// so that the user can try to fix the UTD
NSMutableAttributedString *attributedStringWithRerequestMessage = [attributedString mutableCopy];
[attributedStringWithRerequestMessage appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]];
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"event_formatter_rerequest_keys_part1_link", @"Vector", nil)
attributes:@{
NSLinkAttributeName: linkActionString,
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", kEventFormatterOnReRequestKeysLinkAction,
kEventFormatterOnReRequestKeysLinkActionSeparator,
event.eventId];
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"event_formatter_rerequest_keys_part2", @"Vector", nil)
attributes:@{
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"event_formatter_rerequest_keys_part1_link", @"Vector", nil)
attributes:@{
NSLinkAttributeName: linkActionString,
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
attributedString = attributedStringWithRerequestMessage;
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:NSLocalizedStringFromTable(@"event_formatter_rerequest_keys_part2", @"Vector", nil)
attributes:@{
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
attributedString = attributedStringWithRerequestMessage;
}
}
return attributedString;