Skip to content

Commit 08f447c

Browse files
authored
Add global data collection switch. (#1219)
* Addition of global data collection switch. * Added Messaging conformance to data switch. Also formatted code. * Move data collection flag internal until all SDKs conform to it. * Formatting in response to code review.
1 parent 281d145 commit 08f447c

File tree

7 files changed

+291
-4
lines changed

7 files changed

+291
-4
lines changed

Example/Core/Tests/FIRAppTest.m

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ + (BOOL)validateAppID:(NSString *)appID;
4242
+ (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version;
4343
+ (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version;
4444

45+
+ (nullable NSNumber *)readDataCollectionSwitchFromPlist;
46+
+ (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app;
47+
4548
@end
4649

4750
@interface FIRAppTest : FIRTestCase
@@ -552,6 +555,133 @@ - (void)testAppIDFingerprintInvalid {
552555
[FIRApp validateAppIDFingerprint:@"1:1337:ios:deadbeef:ab" withVersion:kGoodVersionV1]);
553556
}
554557

558+
#pragma mark - Automatic Data Collection Tests
559+
560+
- (void)testGlobalDataCollectionNoFlags {
561+
// Test: No flags set.
562+
[FIRApp configure];
563+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil);
564+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
565+
.andReturn(nil);
566+
567+
XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
568+
}
569+
570+
- (void)testGlobalDataCollectionPlistSetEnabled {
571+
// Test: Plist set to enabled, no override.
572+
[FIRApp configure];
573+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@YES);
574+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
575+
.andReturn(nil);
576+
577+
XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
578+
}
579+
580+
- (void)testGlobalDataCollectionPlistSetDisabled {
581+
// Test: Plist set to disabled, no override.
582+
[FIRApp configure];
583+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@NO);
584+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
585+
.andReturn(nil);
586+
587+
XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
588+
}
589+
590+
- (void)testGlobalDataCollectionUserSpecifiedEnabled {
591+
// Test: User specified as enabled, no plist value.
592+
[FIRApp configure];
593+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil);
594+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
595+
.andReturn(@YES);
596+
597+
XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
598+
}
599+
600+
- (void)testGlobalDataCollectionUserSpecifiedDisabled {
601+
// Test: User specified as disabled, no plist value.
602+
[FIRApp configure];
603+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil);
604+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
605+
.andReturn(@NO);
606+
607+
XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
608+
}
609+
610+
- (void)testGlobalDataCollectionUserOverriddenEnabled {
611+
// Test: User specified as enabled, with plist set as disabled.
612+
[FIRApp configure];
613+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@NO);
614+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
615+
.andReturn(@YES);
616+
617+
XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
618+
}
619+
620+
- (void)testGlobalDataCollectionUserOverriddenDisabled {
621+
// Test: User specified as disabled, with plist set as enabled.
622+
[FIRApp configure];
623+
OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@YES);
624+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
625+
.andReturn(@NO);
626+
627+
XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
628+
}
629+
630+
- (void)testGlobalDataCollectionWriteToDefaults {
631+
id defaultsMock = OCMPartialMock([NSUserDefaults standardUserDefaults]);
632+
[FIRApp configure];
633+
634+
FIRApp *app = [FIRApp defaultApp];
635+
app.automaticDataCollectionEnabled = YES;
636+
NSString *key =
637+
[NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name];
638+
OCMVerify([defaultsMock setObject:@YES forKey:key]);
639+
640+
[FIRApp defaultApp].automaticDataCollectionEnabled = NO;
641+
OCMVerify([defaultsMock setObject:@NO forKey:key]);
642+
643+
[defaultsMock stopMocking];
644+
}
645+
646+
- (void)testGlobalDataCollectionClearedAfterDelete {
647+
// Configure and disable data collection for the default FIRApp.
648+
[FIRApp configure];
649+
FIRApp *app = [FIRApp defaultApp];
650+
app.automaticDataCollectionEnabled = NO;
651+
XCTAssertFalse(app.isAutomaticDataCollectionEnabled);
652+
653+
// Delete the app, and verify that the switch was reset.
654+
XCTestExpectation *deleteFinished =
655+
[self expectationWithDescription:@"The app should successfully delete."];
656+
[app deleteApp:^(BOOL success) {
657+
if (success) {
658+
[deleteFinished fulfill];
659+
}
660+
}];
661+
662+
// Wait for the delete to complete.
663+
[self waitForExpectations:@[ deleteFinished ] timeout:1];
664+
665+
// Set up the default app again, and check the data collection flag.
666+
[FIRApp configure];
667+
XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled);
668+
}
669+
670+
- (void)testGlobalDataCollectionNoDiagnosticsSent {
671+
[FIRApp configure];
672+
673+
// Stub out reading from user defaults since stubbing out the BOOL has issues. If the data
674+
// collection switch is disabled, the `sendLogs` call should return immediately and not fire a
675+
// notification.
676+
OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY])
677+
.andReturn(@NO);
678+
OCMReject([self.notificationCenterMock postNotificationName:kFIRAppDiagnosticsNotification
679+
object:OCMOCK_ANY
680+
userInfo:OCMOCK_ANY]);
681+
NSError *error = [NSError errorWithDomain:@"com.firebase" code:42 userInfo:nil];
682+
[[FIRApp defaultApp] sendLogsWithServiceName:@"Service" version:@"Version" error:error];
683+
}
684+
555685
#pragma mark - Internal Methods
556686

557687
- (void)testAuthGetUID {

Example/Messaging/Tests/FIRMessagingTest.m

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,46 @@ - (void)testAutoInitEnableFlag {
7575
XCTAssertTrue(_messaging.isAutoInitEnabled);
7676
}
7777

78+
- (void)testAutoInitEnableFlagOverrideGlobalTrue {
79+
OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES);
80+
id bundleMock = OCMPartialMock([NSBundle mainBundle]);
81+
OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil);
82+
XCTAssertTrue(self.messaging.isAutoInitEnabled);
83+
84+
self.messaging.autoInitEnabled = NO;
85+
XCTAssertFalse(self.messaging.isAutoInitEnabled);
86+
[bundleMock stopMocking];
87+
}
88+
89+
- (void)testAutoInitEnableFlagOverrideGlobalFalse {
90+
OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES);
91+
id bundleMock = OCMPartialMock([NSBundle mainBundle]);
92+
OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil);
93+
XCTAssertTrue(self.messaging.isAutoInitEnabled);
94+
95+
self.messaging.autoInitEnabled = NO;
96+
XCTAssertFalse(self.messaging.isAutoInitEnabled);
97+
[bundleMock stopMocking];
98+
}
99+
100+
- (void)testAutoInitEnableGlobalDefaultTrue {
101+
OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES);
102+
id bundleMock = OCMPartialMock([NSBundle mainBundle]);
103+
OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil);
104+
105+
XCTAssertTrue(self.messaging.isAutoInitEnabled);
106+
[bundleMock stopMocking];
107+
}
108+
109+
- (void)testAutoInitEnableGlobalDefaultFalse {
110+
OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(NO);
111+
id bundleMock = OCMPartialMock([NSBundle mainBundle]);
112+
OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil);
113+
114+
XCTAssertFalse(self.messaging.isAutoInitEnabled);
115+
[bundleMock stopMocking];
116+
}
117+
78118
#pragma mark - Direct Channel Establishment Testing
79119

80120
// Should connect with valid token and application in foreground

Firebase/Core/FIRApp.m

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
NSString *const kFIRAppNameKey = @"FIRAppNameKey";
4747
NSString *const kFIRGoogleAppIDKey = @"FIRGoogleAppIDKey";
4848

49+
NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat =
50+
@"/google/firebase/global_data_collection_enabled:%@";
51+
NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey =
52+
@"FirebaseAutomaticDataCollectionEnabled";
53+
4954
NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification";
5055

5156
NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"ConfigType";
@@ -227,6 +232,7 @@ - (void)deleteApp:(FIRAppVoidBoolCallback)completion {
227232
if (sAllApps && sAllApps[self.name]) {
228233
FIRLogDebug(kFIRLoggerCore, @"I-COR000006", @"Deleting app named %@", self.name);
229234
[sAllApps removeObjectForKey:self.name];
235+
[self clearDataCollectionSwitchFromUserDefaults];
230236
if ([self.name isEqualToString:kFIRDefaultAppName]) {
231237
sDefaultApp = nil;
232238
}
@@ -332,6 +338,30 @@ - (FIROptions *)options {
332338
return [_options copy];
333339
}
334340

341+
- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled {
342+
NSString *key =
343+
[NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
344+
[[NSUserDefaults standardUserDefaults] setBool:automaticDataCollectionEnabled forKey:key];
345+
}
346+
347+
- (BOOL)isAutomaticDataCollectionEnabled {
348+
// Check if it's been manually set before in code, and use that as the higher priority value.
349+
NSNumber *defaultsObject = [[self class] readDataCollectionSwitchFromUserDefaultsForApp:self];
350+
if (defaultsObject) {
351+
return [defaultsObject boolValue];
352+
}
353+
354+
// Read the Info.plist to see if the flag is set. If it's not set, it should default to `YES`.
355+
// As per the implementation of `readDataCollectionSwitchFromPlist`, it's a cached value and has
356+
// no performance impact calling multiple times.
357+
NSNumber *collectionEnabledPlistValue = [[self class] readDataCollectionSwitchFromPlist];
358+
if (collectionEnabledPlistValue) {
359+
return [collectionEnabledPlistValue boolValue];
360+
}
361+
362+
return YES;
363+
}
364+
335365
#pragma mark - private
336366

337367
+ (void)sendNotificationsToSDKs:(FIRApp *)app {
@@ -613,11 +643,64 @@ - (NSString *)expectedBundleID {
613643
}
614644

615645
// end App ID validation
616-
#pragma mark
646+
647+
#pragma mark - Reading From Plist & User Defaults
648+
649+
/**
650+
* Clears the data collection switch from the standard NSUserDefaults for easier testing and
651+
* readability.
652+
*/
653+
- (void)clearDataCollectionSwitchFromUserDefaults {
654+
NSString *key =
655+
[NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name];
656+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
657+
}
658+
659+
/**
660+
* Reads the data collection switch from the standard NSUserDefaults for easier testing and
661+
* readability.
662+
*/
663+
+ (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app {
664+
// Read the object in user defaults, and only return if it's an NSNumber.
665+
NSString *key =
666+
[NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name];
667+
id collectionEnabledDefaultsObject = [[NSUserDefaults standardUserDefaults] objectForKey:key];
668+
if ([collectionEnabledDefaultsObject isKindOfClass:[NSNumber class]]) {
669+
return collectionEnabledDefaultsObject;
670+
}
671+
672+
return nil;
673+
}
674+
675+
/**
676+
* Reads the data collection switch from the Info.plist for easier testing and readability. Will
677+
* only read once from the plist and return the cached value.
678+
*/
679+
+ (nullable NSNumber *)readDataCollectionSwitchFromPlist {
680+
static NSNumber *collectionEnabledPlistObject;
681+
static dispatch_once_t onceToken;
682+
dispatch_once(&onceToken, ^{
683+
// Read the data from the `Info.plist`, only assign it if it's there and an NSNumber.
684+
id plistValue = [[NSBundle mainBundle]
685+
objectForInfoDictionaryKey:kFIRGlobalAppDataCollectionEnabledPlistKey];
686+
if (plistValue && [plistValue isKindOfClass:[NSNumber class]]) {
687+
collectionEnabledPlistObject = (NSNumber *)plistValue;
688+
}
689+
});
690+
691+
return collectionEnabledPlistObject;
692+
}
693+
694+
#pragma mark - Sending Logs
617695

618696
- (void)sendLogsWithServiceName:(NSString *)serviceName
619697
version:(NSString *)version
620698
error:(NSError *)error {
699+
// If the user has manually turned off data collection, return and don't send logs.
700+
if (![self isAutomaticDataCollectionEnabled]) {
701+
return;
702+
}
703+
621704
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{
622705
kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK),
623706
kFIRAppDiagnosticsSDKNameKey : serviceName,

Firebase/Core/Private/FIRAppInternal.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ extern NSString *const kFIRAppIsDefaultAppKey;
6060
extern NSString *const kFIRAppNameKey;
6161
extern NSString *const kFIRGoogleAppIDKey;
6262

63+
/**
64+
* The format string for the User Defaults key used for storing the data collection enabled flag.
65+
* This includes formatting to append the Firebase App's name.
66+
*/
67+
extern NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat;
68+
69+
/**
70+
* The plist key used for storing the data collection enabled flag.
71+
*/
72+
extern NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey;
73+
74+
/**
75+
* A notification fired containing diagnostic information when SDK errors occur.
76+
*/
77+
extern NSString *const kFIRAppDiagnosticsNotification;
78+
6379
/** @var FIRAuthStateDidChangeInternalNotification
6480
@brief The name of the @c NSNotificationCenter notification which is posted when the auth state
6581
changes (e.g. a new token has been produced, a user logs in or out). The object parameter of
@@ -181,6 +197,18 @@ typedef NSString *_Nullable (^FIRAppGetUIDImplementation)(void);
181197
*/
182198
- (nullable NSString *)getUID;
183199

200+
/**
201+
* WARNING: THIS SETTING DOES NOT WORK YET. IT WILL BE MOVED TO THE PUBLIC HEADER ONCE ALL SDKS
202+
* CONFORM TO THIS PREFERENCE. DO NOT RELY ON IT.
203+
*
204+
* Gets or sets whether automatic data collection is enabled for all products. Defaults to `YES`
205+
* unless `FirebaseAutomaticDataCollectionEnabled` is set to `NO` in your app's Info.plist. This
206+
* value is persisted across runs of the app so that it can be set once when users have consented to
207+
* collection.
208+
*/
209+
@property(nonatomic, readwrite, getter=isAutomaticDataCollectionEnabled)
210+
BOOL automaticDataCollectionEnabled;
211+
184212
@end
185213

186214
NS_ASSUME_NONNULL_END

Firebase/Messaging/FIRMessaging+FIRApp.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ - (void)configureMessaging:(FIRApp *)app {
7272
}
7373

7474
self.fcmSenderID = [options.GCMSenderID copy];
75+
self.globalAutomaticDataCollectionEnabled = [app isAutomaticDataCollectionEnabled];
7576

7677
// Swizzle remote-notification-related methods (app delegate and UNUserNotificationCenter)
7778
if ([FIRMessagingRemoteNotificationsProxy canSwizzleMethods]) {

Firebase/Messaging/FIRMessaging.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575

7676
NSString *const kFIRMessagingAPNSTokenType = @"APNSTokenType"; // APNS Token type key stored in user info.
7777

78-
static NSString *const kFIRMessagingPlistAutoInitEnabled =
78+
NSString *const kFIRMessagingPlistAutoInitEnabled =
7979
@"FirebaseMessagingAutoInitEnabled"; // Auto Init Enabled key stored in Info.plist
8080

8181
@interface FIRMessagingMessageInfo ()
@@ -471,8 +471,9 @@ - (BOOL)isAutoInitEnabled {
471471
if (isAutoInitEnabledObject) {
472472
return [isAutoInitEnabledObject boolValue];
473473
}
474-
// If none of above exists, we default assume FCM auto init is enabled.
475-
return YES;
474+
475+
// If none of above exists, we default to the global switch that comes from FIRApp.
476+
return self.isGlobalAutomaticDataCollectionEnabled;
476477
}
477478

478479
- (void)setAutoInitEnabled:(BOOL)autoInitEnabled {

Firebase/Messaging/FIRMessaging_Private.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) {
2525
kFIRMessagingReachabilityReachableViaWWAN,
2626
};
2727

28+
FOUNDATION_EXPORT NSString *const kFIRMessagingPlistAutoInitEnabled;
2829
FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled;
2930

3031
@interface FIRMessagingRemoteMessage ()
@@ -37,6 +38,9 @@ FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled;
3738

3839
#pragma mark - Private API
3940

41+
// The data collection flag from Core.
42+
@property(nonatomic, readwrite, getter=isGlobalAutomaticDataCollectionEnabled) BOOL globalAutomaticDataCollectionEnabled;
43+
4044
- (NSString *)defaultFcmToken;
4145
- (FIRMessagingClient *)client;
4246
- (FIRMessagingPubSub *)pubsub;

0 commit comments

Comments
 (0)