[ios] add telemetry debug logging

You SHOULD NOT be using telemetry logging on any persons' devices who do
not explicitly understand the privacy implications of handling location
data.
This commit is contained in:
Jason Wray
2016-01-13 16:38:03 -05:00
parent 27271768bd
commit 4a3adddf3f
4 changed files with 249 additions and 21 deletions

View File

@@ -885,3 +885,28 @@ freely, subject to the following restrictions:
Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu
===========================================================================
Mapbox GL uses portions of BugshotKit.
The MIT License (MIT)
Copyright (c) 2014 marcoarment
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -144,9 +144,8 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Reset North",
@"Reset Position",
@"Cycle debug options",
otherButtonTitles:@"Reset Position",
@"Cycle Debug Options",
@"Empty Memory",
@"Add 100 Points",
@"Add 1,000 Points",
@@ -156,6 +155,8 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
@"Add Custom Callout Point",
@"Remove Annotations",
@"Toggle Custom Style Layer",
@"Print Telemetry Logfile",
@"Delete Telemetry Logfile",
nil];
[sheet showFromBarButtonItem:self.navigationItem.leftBarButtonItem animated:YES];
@@ -165,33 +166,29 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
{
if (buttonIndex == actionSheet.firstOtherButtonIndex)
{
[self.mapView resetNorth];
[self.mapView resetPosition];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1)
{
[self.mapView resetPosition];
[self.mapView cycleDebugOptions];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 2)
{
[self.mapView cycleDebugOptions];
[self.mapView emptyMemoryCache];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 3)
{
[self.mapView emptyMemoryCache];
[self parseFeaturesAddingCount:100];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 4)
{
[self parseFeaturesAddingCount:100];
[self parseFeaturesAddingCount:1000];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 5)
{
[self parseFeaturesAddingCount:1000];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 6)
{
[self parseFeaturesAddingCount:10000];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 7)
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 6)
{
// PNW triangle
//
@@ -258,19 +255,19 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
free(polygonCoordinates);
}
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 8)
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 7)
{
[self startWorldTour:actionSheet];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 9)
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 8)
{
[self presentAnnotationWithCustomCallout];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 10)
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 9)
{
[self.mapView removeAnnotations:self.mapView.annotations];
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 11)
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 10)
{
if (_isShowingCustomStyleLayer)
{
@@ -281,6 +278,24 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
[self insertCustomStyleLayer];
}
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 11)
{
NSString *fileContents = [NSString stringWithContentsOfFile:[self telemetryDebugLogfilePath] encoding:NSUTF8StringEncoding error:nil];
NSLog(@"%@", fileContents);
}
else if (buttonIndex == actionSheet.firstOtherButtonIndex + 12)
{
NSString *filePath = [self telemetryDebugLogfilePath];
if ([[NSFileManager defaultManager] isDeletableFileAtPath:filePath]) {
NSError *error;
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (success) {
NSLog(@"Deleted telemetry log.");
} else {
NSLog(@"Error deleting telemetry log: %@", error.localizedDescription);
}
}
}
}
- (void)parseFeaturesAddingCount:(NSUInteger)featuresCount
@@ -481,6 +496,16 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
}];
}
- (NSString *)telemetryDebugLogfilePath
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
[dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", [dateFormatter stringFromDate:[NSDate date]]]];
return filePath;
}
#pragma mark - Destruction
- (void)dealloc

View File

@@ -10,6 +10,7 @@ extern NSString *const MGLEventTypeMapTap;
extern NSString *const MGLEventTypeMapDragEnd;
extern NSString *const MGLEventTypeLocation;
extern NSString *const MGLEventTypeVisit;
extern NSString *const MGLEventTypeLocalDebug;
extern NSString *const MGLEventKeyLatitude;
extern NSString *const MGLEventKeyLongitude;
@@ -24,6 +25,7 @@ extern NSString *const MGLEventKeyEmailEnabled;
extern NSString *const MGLEventKeyGestureID;
extern NSString *const MGLEventKeyArrivalDate;
extern NSString *const MGLEventKeyDepartureDate;
extern NSString *const MGLEventKeyLocalDebugDescription;
extern NSString *const MGLEventGestureSingleTap;
extern NSString *const MGLEventGestureDoubleTap;
@@ -57,6 +59,10 @@ typedef NS_MUTABLE_DICTIONARY_OF(NSString *, id) MGLMutableMapboxEventAttributes
//
+ (void) pushEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary;
+ (void) pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary;
+ (BOOL) debugLoggingEnabled;
// You can call these methods from any thread.
//
+ (BOOL) checkPushEnabled;

View File

@@ -22,6 +22,7 @@ NSString *const MGLEventTypeMapTap = @"map.click";
NSString *const MGLEventTypeMapDragEnd = @"map.dragend";
NSString *const MGLEventTypeLocation = @"location";
NSString *const MGLEventTypeVisit = @"visit";
NSString *const MGLEventTypeLocalDebug = @"debug";
NSString *const MGLEventKeyLatitude = @"lat";
NSString *const MGLEventKeyLongitude = @"lng";
@@ -36,6 +37,7 @@ NSString *const MGLEventKeyEmailEnabled = @"enabled.email";
NSString *const MGLEventKeyGestureID = @"gesture";
NSString *const MGLEventKeyArrivalDate = @"arrivalDate";
NSString *const MGLEventKeyDepartureDate = @"departureDate";
NSString *const MGLEventKeyLocalDebugDescription = @"debug.description";
NSString *const MGLEventGestureSingleTap = @"SingleTap";
NSString *const MGLEventGestureDoubleTap = @"DoubleTap";
@@ -151,6 +153,10 @@ const NSTimeInterval MGLFlushInterval = 60;
//
@property (nonatomic) dispatch_queue_t serialQueue;
@property (atomic) BOOL canEnableDebugLogging;
@property (nonatomic) dispatch_queue_t debugLogSerialQueue;
@property (nonatomic) NSString *dateForDebugLogFile;
@end
@implementation MGLMapboxEvents {
@@ -164,6 +170,7 @@ const NSTimeInterval MGLFlushInterval = 60;
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
@"MGLMapboxAccountType": accountTypeNumber ? accountTypeNumber : @0,
@"MGLMapboxMetricsEnabled": @YES,
@"MGLMapboxMetricsDebugLoggingEnabled": @NO,
}];
}
}
@@ -173,6 +180,15 @@ const NSTimeInterval MGLFlushInterval = 60;
[[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0);
}
- (BOOL)debugLoggingEnabled {
return (self.canEnableDebugLogging &&
[[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]);
}
+ (BOOL)debugLoggingEnabled {
return [[MGLMapboxEvents sharedManager] debugLoggingEnabled];
}
// Must be called from the main thread. Only called internally.
//
- (instancetype) init {
@@ -237,7 +253,19 @@ const NSTimeInterval MGLFlushInterval = 60;
// Enable Battery Monitoring
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
// Configure logging
if ([self isProbablyAppStoreBuild]) {
self.canEnableDebugLogging = NO;
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]) {
NSLog(@"Telemetry logging is only enabled in non-app store builds.");
}
} else {
self.canEnableDebugLogging = YES;
}
// Watch for changes to telemetry settings by the user
__weak MGLMapboxEvents *weakSelf = self;
_userDefaultsObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification
object:nil
@@ -381,9 +409,9 @@ const NSTimeInterval MGLFlushInterval = 60;
_locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
_locationManager.distanceFilter = 10;
_locationManager.delegate = self;
[_locationManager startUpdatingLocation];
// -[CLLocationManager startMonitoringVisits] is only available in iOS 8+.
if ([_locationManager respondsToSelector:@selector(startMonitoringVisits)]) {
[_locationManager startMonitoringVisits];
@@ -419,6 +447,11 @@ const NSTimeInterval MGLFlushInterval = 60;
// Flush
[strongSelf flush];
if ([strongSelf debugLoggingEnabled]) {
[strongSelf writeEventToLocalDebugLog:vevt];
}
});
}
@@ -458,7 +491,7 @@ const NSTimeInterval MGLFlushInterval = 60;
[evt setObject:strongSelf.instanceID forKey:@"instance"];
[evt setObject:strongSelf.data.vendorId forKey:@"vendorId"];
[evt setObject:strongSelf.appBundleId forKeyedSubscript:@"appBundleId"];
// mapbox-events-ios stock attributes
[evt setValue:strongSelf.data.model forKey:@"model"];
[evt setValue:strongSelf.data.iOSVersion forKey:@"operatingSystem"];
@@ -486,6 +519,10 @@ const NSTimeInterval MGLFlushInterval = 60;
// If this is first new event on queue start timer,
[strongSelf startTimer];
}
if ([strongSelf debugLoggingEnabled]) {
[strongSelf writeEventToLocalDebugLog:finalEvent];
}
});
}
@@ -521,6 +558,12 @@ const NSTimeInterval MGLFlushInterval = 60;
strongSelf.timer = nil;
}
});
if ([self debugLoggingEnabled]) {
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{
MGLEventKeyLocalDebugDescription: @"flush"
}];
}
}
// Can be called from any thread. Called implicitly from public
@@ -559,6 +602,13 @@ const NSTimeInterval MGLFlushInterval = 60;
}
}
}
if ([self debugLoggingEnabled]) {
[MGLMapboxEvents pushDebugEvent:MGLEventTypeLocalDebug withAttributes:@{
MGLEventKeyLocalDebugDescription: @"post",
@"debug.eventsCount": @(events.count)
}];
}
}
});
}
@@ -898,4 +948,126 @@ const NSTimeInterval MGLFlushInterval = 60;
}
#pragma mark MGLMapboxEvents Debug
// Can be called from any thread.
//
+ (void) pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[[MGLMapboxEvents sharedManager] pushDebugEvent:event withAttributes:attributeDictionary];
});
}
// Can be called from any thread. Called implicitly from public
// use of +pushDebugEvent:withAttributes:.
//
- (void) pushDebugEvent:(NSString *)event withAttributes:(MGLMapboxEventAttributes *)attributeDictionary {
__weak MGLMapboxEvents *weakSelf = self;
if (![self debugLoggingEnabled] || !event) return;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
MGLMapboxEvents *strongSelf = weakSelf;
if (!strongSelf) return;
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:attributeDictionary];
[evt setObject:event forKey:@"event"];
[evt setObject:[strongSelf.rfc3339DateFormatter stringFromDate:[NSDate date]] forKey:@"created"];
[evt setValue:[strongSelf applicationState] forKey:@"applicationState"];
[evt setValue:@([[self class] isEnabled]) forKey:@"telemetryEnabled"];
[evt setObject:strongSelf.instanceID forKey:@"instance"];
// Make immutable version
MGLMapboxEventAttributes *finalEvent = [NSDictionary dictionaryWithDictionary:evt];
[strongSelf writeEventToLocalDebugLog:finalEvent];
});
}
- (void) writeEventToLocalDebugLog:(MGLMapboxEventAttributes *)event {
if (![self debugLoggingEnabled]) return;
NSLog(@"%@", [self stringForDebugEvent:event]);
if ( ! self.dateForDebugLogFile) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
[dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
self.dateForDebugLogFile = [dateFormatter stringFromDate:[NSDate date]];
}
if ( ! _debugLogSerialQueue) {
NSString *uniqueID = [[NSProcessInfo processInfo] globallyUniqueString];
_debugLogSerialQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@.%@.events.debugLog", _appBundleId, uniqueID] UTF8String], DISPATCH_QUEUE_SERIAL);
}
dispatch_sync(_debugLogSerialQueue, ^{
if ([NSJSONSerialization isValidJSONObject:event]) {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsonString = [jsonString stringByAppendingString:@",\n"];
NSString *logFilePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", self.dateForDebugLogFile]];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:logFilePath]) {
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
} else {
[fileManager createFileAtPath:logFilePath contents:[jsonString dataUsingEncoding:NSUTF8StringEncoding] attributes:@{ NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication }];
}
}
});
}
- (NSString *) stringForDebugEvent:(MGLMapboxEventAttributes *)event {
// redact potentially sensitive location details from system console log
if ([event[@"event"] isEqualToString:MGLEventTypeLocation] ||
[event[@"event"] isEqualToString:MGLEventTypeVisit]) {
MGLMutableMapboxEventAttributes *evt = [MGLMutableMapboxEventAttributes dictionaryWithDictionary:event];
[evt setObject:@"<redacted>" forKey:@"lat"];
[evt setObject:@"<redacted>" forKey:@"lng"];
event = evt;
}
return [NSString stringWithFormat:@"Mapbox Telemetry event %@", event];
}
- (BOOL) isProbablyAppStoreBuild {
#if TARGET_IPHONE_SIMULATOR
return NO;
#else
// BugshotKit by Marco Arment https://github.com/marcoarment/BugshotKit/
// Adapted from https://github.com/blindsightcorp/BSMobileProvision
NSString *binaryMobileProvision = [NSString stringWithContentsOfFile:[NSBundle.mainBundle pathForResource:@"embedded" ofType:@"mobileprovision"] encoding:NSISOLatin1StringEncoding error:NULL];
if (! binaryMobileProvision) return YES; // no provision
NSScanner *scanner = [NSScanner scannerWithString:binaryMobileProvision];
NSString *plistString;
if (! [scanner scanUpToString:@"<plist" intoString:nil] || ! [scanner scanUpToString:@"</plist>" intoString:&plistString]) return YES; // no XML plist found in provision
plistString = [plistString stringByAppendingString:@"</plist>"];
NSData *plistdata_latin1 = [plistString dataUsingEncoding:NSISOLatin1StringEncoding];
NSError *error = nil;
NSDictionary *mobileProvision = [NSPropertyListSerialization propertyListWithData:plistdata_latin1 options:NSPropertyListImmutable format:NULL error:&error];
if (error) return YES; // unknown plist format
if (! mobileProvision || ! mobileProvision.count) return YES; // no entitlements
if (mobileProvision[@"ProvisionsAllDevices"]) return NO; // enterprise provisioning
if (mobileProvision[@"ProvisionedDevices"] && [mobileProvision[@"ProvisionedDevices"] count]) return NO; // development or ad-hoc
return YES; // expected development/enterprise/ad-hoc entitlements not found
#endif
}
@end