Files
MapLibreParticleLab/platform/ios/src/MGLMapboxEvents.m
Jesse Bounds 2e3145db47 Replace embedded telem implementation with mapbox-mobile-events library (#10698)
* [ios] Introduce mobile events dependency

This adds the mobile events dependency including the events library's namespaced header file so that the related compiled symbols are prefixed to make them specific to the maps SDK. A pre-compiled header file is added so that the namespaced header file is added in all places where it is needed (most importantly every events library file).

* [ios] Use new events library for telemetry events

This makes the following significant changes:

Refactors the MGLMapboxEvents singleton to delegate internally to an instance of MMEEventsManager that it keeps for itself. The MGLMapboxEvents public API is refactored to reflect this. Note: MGLMapboxEvents continues to handle checking for opt out

Uses that new events API in MGLMapboxEvents to send all telem events (and turnstile)

All embedded certs are removed since the new telem library uses public key info pinning

Legacy telemetry utility classes for location and networking are removed since those are implemented in the telem library

* [ios] Update submodules initialized by cmake

We no longer bring in SMCalloutView as a submodule so it is removed.
We now vendor the telemetry events library as a submodule so it is added.

* Update mapbox-mobile-events

* Update mobile events lib

* Set events options with MGL user defaults values

For options that historically could be set with user defaults in the Maps SDK, collect them and set the appropriate property values in the new events library.

Also, check the existence of two new optional configuration values MGLTelemetryAccessToken and MGLTelemetryBaseURL so that the events endpoint and access token can be configured and the access token can easily be set dynamically and apart from the access token for the maps API.

MGLTelemetryBaseURL replaces the legacy MGLTelemetryTestServerURL. This is ok because this value was never intended to be used by client applications. The new name better reflects the fact that the URL can be changed to any backend stack, not just a test server.

Because it is possible for the configuration values to be read from user defaults before the events manager is fully set up, this also adds a local cache for the baseURL and accessToken values so that they can be stored and applied once it is time to setup the the events manager.

* Update mapbox-mobile-events

mapbox-mobile-events was forced pushed to include https://github.com/mapbox/mapbox-events-ios/pull/28

* Rename MGLMapboxEvents singleton access method

Manager is not in the name of the class. Just call it an instance.

* Update mapbox-mobile-events

* Add comments about config value loading

* Guard against creating events for IB

* Refactor user defaults update handler

This updates the handling logic for user defaults to break apart config changes that require a check for pausing or resuming the events lib’s telemetry collection from config changes that can happen with no update to the pause/resume state.

It also ports the optimization from https://github.com/mapbox/mapbox-gl-native/pull/10803 so that the call to pause and resume is not performed unless the user defaults store has new values that have not yet been applied to the events library.

* Update mapbox-mobile-events

* Rename events submodule folder

* Fix incorrect events library refs

* Update mapbox-mobile-events

* Update user user agent id value

This value aligns with our schme of {source-sdk-platform} and will be used by the events library in the user agent and vendorid.

* Update mapbox-mobile-events

Pin to v3.0.0 (d522b18)

* Improve code comments

* [ios] Fixed a broken file reference to MGLTelemetryConfig.
2018-02-20 09:54:11 -05:00

187 lines
9.7 KiB
Objective-C

#import "MGLMapboxEvents.h"
#import "NSBundle+MGLAdditions.h"
#import "NSProcessInfo+MGLAdditions.h"
static NSString * const MGLAPIClientUserAgentBase = @"mapbox-maps-ios";
static NSString * const MGLMapboxAccountType = @"MGLMapboxAccountType";
static NSString * const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled";
static NSString * const MGLMapboxMetricsDebugLoggingEnabled = @"MGLMapboxMetricsDebugLoggingEnabled";
static NSString * const MGLTelemetryAccessToken = @"MGLTelemetryAccessToken";
static NSString * const MGLTelemetryBaseURL = @"MGLTelemetryBaseURL";
@interface MGLMapboxEvents ()
@property (nonatomic) MMEEventsManager *eventsManager;
@property (nonatomic) NSURL *baseURL;
@property (nonatomic, copy) NSString *accessToken;
@end
@implementation MGLMapboxEvents
+ (void)initialize {
if (self == [MGLMapboxEvents class]) {
NSBundle *bundle = [NSBundle mainBundle];
NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:MGLMapboxAccountType];
[[NSUserDefaults standardUserDefaults] registerDefaults:@{MGLMapboxAccountType: accountTypeNumber ?: @0,
MGLMapboxMetricsEnabled: @YES,
MGLMapboxMetricsDebugLoggingEnabled: @NO}];
}
}
+ (nullable instancetype)sharedInstance {
if (NSProcessInfo.processInfo.mgl_isInterfaceBuilderDesignablesAgent) {
return nil;
}
static dispatch_once_t onceToken;
static MGLMapboxEvents *_sharedInstance;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
- (instancetype)init {
self = [super init];
if (self) {
_eventsManager = [[MMEEventsManager alloc] init];
_eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabled];
_eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType];
_eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled];
// It is possible for the shared instance of this class to be created because of a call to
// +[MGLAccountManager load] early on in the app lifecycle of the host application.
// If user default values for access token and base URL are available, they are stored here
// on local properties so that they can be applied later once MMEEventsManager is fully initialized
// (once -[MMEEventsManager initializeWithAccessToken:userAgentBase:hostSDKVersion:] is called.
// Normally, the telem access token and base URL are not set this way. However, overriding these values
// with user defaults can be useful for testing with an alternative (test) backend system.
if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) {
self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken];
}
if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) {
self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)userDefaultsDidChange:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateNonDisablingConfigurationValues];
[self updateDisablingConfigurationValuesWithNotification:notification];
});
}
- (void)updateNonDisablingConfigurationValues {
self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"];
// It is possible for `MGLTelemetryAccessToken` to have been set yet `userDefaultsDidChange:`
// is called before `setupWithAccessToken:` is called.
// In that case, setting the access token here will have no effect. In practice, that's fine
// because the access token value will be resolved when `setupWithAccessToken:` is called eventually
if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) {
self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken];
}
// It is possible for `MGLTelemetryBaseURL` to have been set yet `userDefaultsDidChange:`
// is called before setupWithAccessToken: is called.
// In that case, setting the base URL here will have no effect. In practice, that's fine
// because the base URL value will be resolved when `setupWithAccessToken:` is called eventually
if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) {
NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]];
self.eventsManager.baseURL = baseURL;
}
}
- (void)updateDisablingConfigurationValuesWithNotification:(NSNotification *)notification {
// Guard against over calling pause / resume if the values this implementation actually
// cares about have not changed. We guard because the pause and resume method checks CoreLocation's
// authorization status and that can drag on the main thread if done too many times (e.g. if the host
// app heavily uses the user defaults API and this method is called very frequently)
if ([[notification object] respondsToSelector:@selector(objectForKey:)]) {
NSUserDefaults *userDefaults = [notification object];
NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountType];
BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabled];
if (accountType != self.eventsManager.accountType || metricsEnabled != self.eventsManager.metricsEnabled) {
self.eventsManager.accountType = accountType;
self.eventsManager.metricsEnabled = metricsEnabled;
[self.eventsManager pauseOrResumeMetricsCollectionIfRequired];
}
}
}
+ (void)setupWithAccessToken:(NSString *)accessToken {
NSString *semanticVersion = [NSBundle mgl_frameworkInfoDictionary][@"MGLSemanticVersionString"];
NSString *shortVersion = [NSBundle mgl_frameworkInfoDictionary][@"CFBundleShortVersionString"];
NSString *sdkVersion = semanticVersion ?: shortVersion;
// It is possible that an alternative access token was already set on this instance when the class was loaded
// Use it if it exists
NSString *resolvedAccessToken = [MGLMapboxEvents sharedInstance].accessToken ?: accessToken;
[[[self sharedInstance] eventsManager] initializeWithAccessToken:resolvedAccessToken userAgentBase:MGLAPIClientUserAgentBase hostSDKVersion:sdkVersion];
// It is possible that an alternative base URL was set on this instance when the class was loaded
// Use it if it exists
if ([MGLMapboxEvents sharedInstance].baseURL) {
[[MGLMapboxEvents sharedInstance] eventsManager].baseURL = [MGLMapboxEvents sharedInstance].baseURL;
}
}
+ (void)pushTurnstileEvent {
[[[self sharedInstance] eventsManager] sendTurnstileEvent];
}
+ (void)pushEvent:(NSString *)event withAttributes:(MMEMapboxEventAttributes *)attributeDictionary {
[[[self sharedInstance] eventsManager] enqueueEventWithName:event attributes:attributeDictionary];
}
+ (void)flush {
[[[self sharedInstance] eventsManager] flush];
}
+ (void)ensureMetricsOptoutExists {
NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"];
BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue];
if (!metricsEnabledSettingShownInAppFlag &&
[[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0) {
// Opt-out is not configured in UI, so check for Settings.bundle
id defaultEnabledValue;
NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
if (appSettingsBundle) {
// Dynamic Settings.bundle loading based on http://stackoverflow.com/a/510329/2094275
NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]];
NSArray *preferences = settings[@"PreferenceSpecifiers"];
for (NSDictionary *prefSpecification in preferences) {
if ([prefSpecification[@"Key"] isEqualToString:MGLMapboxMetricsEnabled]) {
defaultEnabledValue = prefSpecification[@"DefaultValue"];
}
}
}
if (!defaultEnabledValue) {
[NSException raise:@"Telemetry opt-out missing" format:
@"End users must be able to opt out of Mapbox Telemetry in your app, either inside Settings (via Settings.bundle) or inside this app. "
@"By default, this opt-out control is included as a menu item in the attribution action sheet. "
@"If you reimplement the opt-out control inside this app, disable this assertion by setting MGLMapboxMetricsEnabledSettingShownInApp to YES in Info.plist."
@"\n\nSee https://www.mapbox.com/ios-sdk/#telemetry_opt_out for more information."
@"\n\nAdditionally, by hiding this attribution control you agree to display the required attribution elsewhere in this app."];
}
}
}
@end