From 4a3adddf3f4f833768f680d2d1e228c88f2e06e3 Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Wed, 13 Jan 2016 16:38:03 -0500 Subject: [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. --- LICENSE.md | 25 ++++++ ios/app/MBXViewController.mm | 59 ++++++++---- platform/ios/src/MGLMapboxEvents.h | 6 ++ platform/ios/src/MGLMapboxEvents.m | 180 ++++++++++++++++++++++++++++++++++++- 4 files changed, 249 insertions(+), 21 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 095660f87a..e1d6f55fe5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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. diff --git a/ios/app/MBXViewController.mm b/ios/app/MBXViewController.mm index 57817d752b..6a2b0cc3fa 100644 --- a/ios/app/MBXViewController.mm +++ b/ios/app/MBXViewController.mm @@ -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]; @@ -164,34 +165,30 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = { - (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex { if (buttonIndex == actionSheet.firstOtherButtonIndex) - { - [self.mapView resetNorth]; - } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1) { [self.mapView resetPosition]; } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 2) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1) { [self.mapView cycleDebugOptions]; } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 3) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 2) { [self.mapView emptyMemoryCache]; } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 4) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 3) { [self parseFeaturesAddingCount:100]; } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 5) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 4) { [self parseFeaturesAddingCount:1000]; } - else if (buttonIndex == actionSheet.firstOtherButtonIndex + 6) + else if (buttonIndex == actionSheet.firstOtherButtonIndex + 5) { [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 diff --git a/platform/ios/src/MGLMapboxEvents.h b/platform/ios/src/MGLMapboxEvents.h index dba24885bf..6dbe93f64a 100644 --- a/platform/ios/src/MGLMapboxEvents.h +++ b/platform/ios/src/MGLMapboxEvents.h @@ -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; diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index bde735f224..6654cc6ac5 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -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:@"" forKey:@"lat"]; + [evt setObject:@"" 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:@"" intoString:&plistString]) return YES; // no XML plist found in provision + plistString = [plistString stringByAppendingString:@""]; + + 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 -- cgit v1.2.1