Skip to content

Commit

Permalink
feat(analytics): add setConsent implementation (#7629)
Browse files Browse the repository at this point in the history
  • Loading branch information
Summys committed Feb 19, 2024
1 parent 2ff569d commit 7816985
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 21 deletions.
13 changes: 13 additions & 0 deletions docs/analytics/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,19 @@ import { firebase } from '@react-native-firebase/analytics';
await firebase.analytics().setAnalyticsCollectionEnabled(true);
```

To update user's consent (e.g. once you have the users consent), call the `setConsent` method:

```js
import { firebase } from '@react-native-firebase/analytics';
// ...
await firebase.analytics().setConsent({
analytics_storage: true,
ad_storage: true,
ad_user_data: true,
ad_personalization: true,
});
```

## Disable screenview tracking

Analytics automatically tracks some information about screens in your application, such as the class name of the UIViewController or Activity that is currently in focus.
Expand Down
25 changes: 25 additions & 0 deletions packages/analytics/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,31 @@ describe('Analytics', function () {
);
});

it('throws if consentSettings is not an object', function () {
// @ts-ignore test
expect(() => firebase.analytics().setConsent(1337)).toThrowError(
'The supplied arg must be an object of key/values.',
);
});
it('throws if consentSettings is invalid', function () {
const consentSettings = {
ad_storage: true,
foo: {
bar: 'baz',
},
};
// @ts-ignore test
expect(() => firebase.analytics().setConsent(consentSettings)).toThrowError(
"'consentSettings' value for parameter 'foo' is invalid, expected a boolean.",
);
});
it('throws if one value of consentSettings is a number', function () {
// @ts-ignore test
expect(() => firebase.analytics().setConsent({ ad_storage: 123 })).toThrowError(
"'consentSettings' value for parameter 'ad_storage' is invalid, expected a boolean.",
);
});

it('errors when no parameters are set', function () {
// @ts-ignore test
expect(() => firebase.analytics().logSearch()).toThrowError(
Expand Down
27 changes: 21 additions & 6 deletions packages/analytics/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ apply from: file('./../../app/android/firebase-json.gradle')
String collectionDeactivated = 'false'
String adidEnabled = 'true'
String ssaidEnabled = 'true'
String personalizationEnabled = 'true'
String automaticScreenReportingEnabled = 'true'
String analyticsStorageEnabled = 'true'
String adStorageEnabled = 'true'
String adUserDataEnabled = 'true'
String personalizationEnabled = 'true'

// If nothing is defined, data collection defaults to true
String dataCollectionEnabled = 'true'
Expand Down Expand Up @@ -96,12 +99,21 @@ if (rootProject.ext && rootProject.ext.firebaseJson) {
if (rnfbConfig.isFlagEnabled('google_analytics_ssaid_collection_enabled', true) == false) {
ssaidEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('analytics_default_allow_ad_personalization_signals', true) == false) {
personalizationEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('google_analytics_automatic_screen_reporting_enabled', true) == false) {
automaticScreenReportingEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('analytics_default_allow_analytics_storage', true) == false) {
analyticsStorageEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('analytics_default_allow_ad_storage', true) == false) {
adStorageEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('analytics_default_allow_ad_user_data', true) == false) {
adUserDataEnabled = 'false'
}
if (rnfbConfig.isFlagEnabled('analytics_default_allow_ad_personalization_signals', true) == false) {
personalizationEnabled = 'false'
}
}

android {
Expand All @@ -117,8 +129,11 @@ android {
firebaseJsonCollectionDeactivated: collectionDeactivated,
firebaseJsonAdidEnabled: adidEnabled,
firebaseJsonSsaidEnabled: ssaidEnabled,
firebaseJsonPersonalizationEnabled: personalizationEnabled,
firebaseJsonAutomaticScreenReportingEnabled: automaticScreenReportingEnabled
firebaseJsonAutomaticScreenReportingEnabled: automaticScreenReportingEnabled,
firebaseJsonAnalyticsStorageEnabled: analyticsStorageEnabled,
firebaseJsonAdStorageEnabled: adStorageEnabled,
firebaseJsonAdUserDataEnabled: adUserDataEnabled,
firebaseJsonPersonalizationEnabled: personalizationEnabled
]
}

Expand Down
6 changes: 5 additions & 1 deletion packages/analytics/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="${firebaseJsonCollectionDeactivated}" />
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="${firebaseJsonAdidEnabled}" />
<meta-data android:name="google_analytics_ssaid_collection_enabled" android:value="${firebaseJsonSsaidEnabled}" />
<meta-data android:name="google_analytics_default_allow_ad_personalization_signals" android:value="${firebaseJsonPersonalizationEnabled}" />
<meta-data android:name="google_analytics_automatic_screen_reporting_enabled" android:value="${firebaseJsonAutomaticScreenReportingEnabled}" />
<meta-data android:name="google_analytics_default_allow_analytics_storage" android:value="${firebaseJsonAnalyticsStorageEnabled}" />
<meta-data android:name="google_analytics_default_allow_ad_storage" android:value="${firebaseJsonAdStorageEnabled}" />
<meta-data android:name="google_analytics_default_allow_ad_user_data" android:value="${firebaseJsonAdUserDataEnabled}" />
<meta-data android:name="google_analytics_default_allow_ad_personalization_signals" android:value="${firebaseJsonPersonalizationEnabled}" />

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.analytics.FirebaseAnalytics.ConsentStatus;
import com.google.firebase.analytics.FirebaseAnalytics.ConsentType;
import io.invertase.firebase.common.UniversalFirebaseModule;
import java.util.EnumMap;
import java.util.Map;
import java.util.Set;

@SuppressWarnings("WeakerAccess")
Expand Down Expand Up @@ -109,4 +113,29 @@ Task<Void> setDefaultEventParameters(Bundle parameters) {
return null;
});
}

Task<Void> setConsent(Bundle consentSettings) {
return Tasks.call(
() -> {
boolean analyticsStorage = consentSettings.getBoolean("analytics_storage");
boolean adStorage = consentSettings.getBoolean("ad_storage");
boolean adUserData = consentSettings.getBoolean("ad_user_data");
boolean adPersonalization = consentSettings.getBoolean("ad_personalization");

Map<ConsentType, ConsentStatus> consentMap = new EnumMap<>(ConsentType.class);
consentMap.put(
ConsentType.ANALYTICS_STORAGE,
analyticsStorage ? ConsentStatus.GRANTED : ConsentStatus.DENIED);
consentMap.put(
ConsentType.AD_STORAGE, adStorage ? ConsentStatus.GRANTED : ConsentStatus.DENIED);
consentMap.put(
ConsentType.AD_USER_DATA, adUserData ? ConsentStatus.GRANTED : ConsentStatus.DENIED);
consentMap.put(
ConsentType.AD_PERSONALIZATION,
adPersonalization ? ConsentStatus.GRANTED : ConsentStatus.DENIED);

FirebaseAnalytics.getInstance(getContext()).setConsent(consentMap);
return null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@ public void setDefaultEventParameters(@Nullable ReadableMap params, Promise prom
});
}

@ReactMethod
public void setConsent(ReadableMap consentSettings, Promise promise) {
module
.setConsent(Arguments.toBundle(consentSettings))
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
promise.resolve(task.getResult());
} else {
rejectPromiseWithExceptionMap(promise, task.getException());
}
});
}

private Bundle toBundle(ReadableMap readableMap) {
Bundle bundle = Arguments.toBundle(readableMap);
if (bundle == null) {
Expand Down
34 changes: 34 additions & 0 deletions packages/analytics/e2e/analytics.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,22 @@ describe('analytics() modular', function () {
});
});

describe('setConsent()', function () {
it('set ad_storage=true on consentSettings', async function () {
const consentSettings = {
ad_storage: true,
};
await firebase.analytics().setConsent(consentSettings);
});
it('set ad_storage=false and analytics_storage=true on consentSettings', async function () {
const consentSettings = {
ad_storage: false,
analytics_storage: true,
};
await firebase.analytics().setConsent(consentSettings);
});
});

// Test this one near end so all the previous hits are visible in DebugView is that is enabled
describe('resetAnalyticsData()', function () {
it('calls native fn without error', async function () {
Expand Down Expand Up @@ -1068,5 +1084,23 @@ describe('analytics() modular', function () {
await setAnalyticsCollectionEnabled(getAnalytics(), true);
});
});

describe('setConsent()', function () {
it('set ad_storage=true on consentSettings', async function () {
const consentSettings = {
ad_storage: true,
};
const { getAnalytics, setConsent } = analyticsModular;
await setConsent(getAnalytics(), consentSettings);
});
it('set ad_storage=false and analytics_storage=true on consentSettings', async function () {
const consentSettings = {
ad_storage: false,
analytics_storage: true,
};
const { getAnalytics, setConsent } = analyticsModular;
await setConsent(getAnalytics(), consentSettings);
});
});
});
});
23 changes: 23 additions & 0 deletions packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@ - (dispatch_queue_t)methodQueue {
return resolve([NSNull null]);
}

RCT_EXPORT_METHOD(setConsent
: (NSDictionary *)consentSettings resolver
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
@try {
BOOL analyticsStorage = [consentSettings[@"analytics_storage"] boolValue];
BOOL adStorage = [consentSettings[@"ad_storage"] boolValue];
BOOL adUserData = [consentSettings[@"ad_user_data"] boolValue];
BOOL adPersonalization = [consentSettings[@"ad_personalization"] boolValue];
[FIRAnalytics setConsent:@{
FIRConsentTypeAnalyticsStorage : analyticsStorage ? FIRConsentStatusGranted
: FIRConsentStatusDenied,
FIRConsentTypeAdStorage : adStorage ? FIRConsentStatusGranted : FIRConsentStatusDenied,
FIRConsentTypeAdUserData : adUserData ? FIRConsentStatusGranted : FIRConsentStatusDenied,
FIRConsentTypeAdPersonalization : adPersonalization ? FIRConsentStatusGranted
: FIRConsentStatusDenied,
}];
} @catch (NSException *exception) {
return [RNFBSharedUtils rejectPromiseWithExceptionDict:reject exception:exception];
}
return resolve([NSNull null]);
}

#pragma mark -
#pragma mark Private methods

Expand Down
34 changes: 30 additions & 4 deletions packages/analytics/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,15 +774,19 @@ export namespace FirebaseAnalyticsTypes {
*/
export interface ConsentSettings {
/** Enables storage, such as cookies, related to advertising */
ad_storage?: ConsentStatusString;
ad_storage?: boolean;
/** Sets consent for sending user data to Google for online advertising purposes */
ad_user_data?: boolean;
/** Sets consent for personalized advertising */
ad_personalization?: boolean;
/** Enables storage, such as cookies, related to analytics (for example, visit duration) */
analytics_storage?: ConsentStatusString;
analytics_storage?: boolean;
/**
* Enables storage that supports the functionality of the website or app such as language settings
*/
functionality_storage?: ConsentStatusString;
functionality_storage?: boolean;
/** Enables storage related to personalization such as video recommendations */
personalization_storage?: ConsentStatusString;
personalization_storage?: boolean;
/**
* Enables storage related to security such as authentication functionality, fraud prevention,
* and other user protection.
Expand Down Expand Up @@ -1727,6 +1731,28 @@ export namespace FirebaseAnalyticsTypes {
* @param phoneNumber phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces.
*/
initiateOnDeviceConversionMeasurementWithPhoneNumber(phoneNumber: string): Promise<void>;

/**
* For Consent Mode!
*
* #### Example
*
* ```js
* // Disable consent
* await firebase.analytics().setConsent({
* ad_personalization: false,
* analytics_storage: false,
* ad_storage: false,
* ad_user_data: false,
* });
* ```
*
* Sets the applicable end user consent state (e.g., for device identifiers) for this app on this device.
* Use the consent map to specify individual consent type values.
* Settings are persisted across app sessions.
* @param consentSettings Consent status settings for each consent type.
*/
setConsent(consentSettings: ConsentSettings): Promise<void>;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/analytics/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export {
setDefaultEventParameters,
initiateOnDeviceConversionMeasurementWithEmailAddress,
initiateOnDeviceConversionMeasurementWithPhoneNumber,
setConsent,
} from './modular/index';

const ReservedEventNames = [
Expand Down Expand Up @@ -261,6 +262,26 @@ class FirebaseAnalyticsModule extends FirebaseModule {
return this.native.resetAnalyticsData();
}

setConsent(consentSettings) {
if (!isObject(consentSettings)) {
throw new Error(
'firebase.analytics().setConsent(*): The supplied arg must be an object of key/values.',
);
}

const entries = Object.entries(consentSettings);
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i];
if (!isBoolean(value)) {
throw new Error(
`firebase.analytics().setConsent(*) 'consentSettings' value for parameter '${key}' is invalid, expected a boolean.`,
);
}
}

return this.native.setConsent(consentSettings);
}

/** -------------------
* EVENTS
* -------------------- */
Expand Down
8 changes: 4 additions & 4 deletions packages/analytics/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1189,16 +1189,16 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(
export function isSupported(): Promise<boolean>;

/**
* Sets the applicable end user consent state for this web app across all gtag
* references once Firebase Analytics is initialized. Web only.
* Sets the applicable end user consent state for this app.
* references once Firebase Analytics is initialized.
* @param analytics Analytics instance.
* @param consentSettings See {@link analytics.ConsentSettings}.
* @returns {void}
* @returns {Promise<void>}
*/
export function setConsent(
analytics: Analytics,
consentSettings: FirebaseAnalyticsTypes.ConsentSettings,
): void;
): Promise<void>;

/**
* Configures Firebase Analytics to use custom gtag or dataLayer names.
Expand Down
10 changes: 5 additions & 5 deletions packages/analytics/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,15 +648,15 @@ export function isSupported() {
}

/**
* Sets the applicable end user consent state for this web app across all gtag
* references once Firebase Analytics is initialized. Web only.
* Sets the applicable end user consent state for this app.
* references once Firebase Analytics is initialized.
* @param analytics Analytics instance.
* @param consentSettings See {@link analytics.ConsentSettings}.
* @returns {void}
* @returns {Promise<void>}
*/
// eslint-disable-next-line
export function setConsent(consentSettings) {
// Returns nothing until Web implemented.
export function setConsent(analytics, consentSettings) {
return analytics.setConsent(consentSettings);
}

/**
Expand Down
Loading

0 comments on commit 7816985

Please sign in to comment.