Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(analytics): add mode property to Firebase integration #67

Merged
merged 1 commit into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/react-native-analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"devDependencies": {
"@castleio/react-native-castle": "^1.1.5",
"@farfetch/blackout-core": "^1.58.2",
"@farfetch/blackout-core": "^1.79.0",
"@react-native-async-storage/async-storage": "^1.16.0",
"@react-native-firebase/analytics": "^18.9.0",
"@react-native-firebase/app": "^18.9.0",
Expand All @@ -23,7 +23,7 @@
},
"peerDependencies": {
"@castleio/react-native-castle": "^1.1.5",
"@farfetch/blackout-core": "^1.58.2",
"@farfetch/blackout-core": "^1.79.0",
"@react-native-async-storage/async-storage": "^1.6.1",
"@react-native-firebase/analytics": "^18.9.0",
"react-native": "^0.62.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ Object {
"ADDRESS_INFO_ADDED": "Address Info Added",
"APP_CLOSED": "App Closed",
"APP_OPENED": "App Opened",
"BILLING_INFO_ADDED": "Billing Info Added",
"CHECKOUT_ABANDONED": "Checkout Abandoned",
"CHECKOUT_STARTED": "Checkout Started",
"CHECKOUT_STEP_COMPLETED": "Checkout Step Completed",
"CHECKOUT_STEP_EDITING": "Checkout Step Editing",
"CHECKOUT_STEP_VIEWED": "Checkout Step Viewed",
"DELIVERY_METHOD_ADDED": "Delivery Method Added",
"FILTERS_APPLIED": "Filters Applied",
"FILTERS_CLEARED": "Filters Cleared",
"INTERACT_CONTENT": "Interact Content",
Expand Down Expand Up @@ -37,5 +39,6 @@ Object {
"SIGNUP_FORM_COMPLETED": "Sign-up Form Completed",
"SIGNUP_FORM_VIEWED": "Sign-up Form Viewed",
"SIGNUP_NEWSLETTER": "Sign-up Newsletter",
"SITE_PERFORMANCE": "Site Performance",
}
`;
14 changes: 12 additions & 2 deletions packages/react-native-analytics/src/__tests__/analytics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ describe('analytics react native', () => {
type: trackTypes.TRACK,
event,
properties,
context: expect.objectContaining({ event: eventContext }),
context: expect.objectContaining({
event: expect.objectContaining({
...eventContext,
__blackoutAnalyticsEventId: expect.any(String),
}),
}),
}),
);

Expand All @@ -99,7 +104,12 @@ describe('analytics react native', () => {
type: trackTypes.SCREEN,
event,
properties,
context: expect.objectContaining({ event: eventContext }),
context: expect.objectContaining({
event: expect.objectContaining({
...eventContext,
__blackoutAnalyticsEventId: expect.any(String),
}),
}),
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { trackTypes as analyticsTrackTypes } from '@farfetch/blackout-core/analytics';
import {
trackTypes as analyticsTrackTypes,
utils,
} from '@farfetch/blackout-core/analytics';

export default function generateAnalyticsEventData(
trackType = analyticsTrackTypes.TRACK,
Expand All @@ -13,7 +16,10 @@ export default function generateAnalyticsEventData(
device: 'iPhone13,2',
deviceLanguage: 'en',
deviceOS: 'iOS 14.3',
event: null,
event: {
[utils.ANALYTICS_UNIQUE_EVENT_ID]:
'179373c4-5651-40fe-8a50-66c3d7c86912',
},
library: {
version: '1.15.0-chore-FPSCH-625-add-support-for-site-features.0',
name: '@farfetch/blackout-core/analytics',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getVirtualEventsFromEvent,
} from './defaultMappers';
import get from 'lodash/get';
import omit from 'lodash/omit';
import Integration from '../integration';
import screenTypes from '../../screenTypes';

Expand Down Expand Up @@ -58,18 +59,27 @@ class FirebaseAnalytics extends Integration {
this.initialize(options);
this.onSetUser(loadData, options);
this.googleConsentConfig = options[OPTION_GOOGLE_CONSENT_CONFIG];
this.googleConsentConfigWithoutMode = omit(this.googleConsentConfig, [
'mode',
]);
}

/**
* Method to check if the integration is ready to be loaded.
* If the googleConsentConfig.mode property is set to 'Advanced'
* the integration will be loaded regardless of user consent.
* Else, only if statistics consent is given by the user.
*
* @static
*
* @param {object} consent - The consent object representing the user preferences.
* @param consent - User consent data.
* @param options - Options passed for the Firebase integration.
*
* @returns {boolean} - If the integration is ready to be loaded.
* @returns If the integration is ready to be loaded.
*/
static shouldLoad(consent) {
static shouldLoad(consent, options) {
if (get(options, `${OPTION_GOOGLE_CONSENT_CONFIG}.mode`) === 'Advanced') {
return true;
}

return !!consent && !!consent.statistics;
}

Expand Down Expand Up @@ -374,32 +384,41 @@ class FirebaseAnalytics extends Integration {
* @param {object} consentData - Consent object containing the user consent.
*/
async setConsent(consentData) {
if (this.googleConsentConfig) {
if (this.googleConsentConfigWithoutMode) {
// Dealing with null or undefined consent values
const safeConsent = consentData || {};

// Fill consent value into consent element, using analytics consent categories
const consentValues = Object.keys(this.googleConsentConfig).reduce(
(result, consentKey) => {
let consentValue = false;

const consent = this.googleConsentConfig[consentKey];

if (consent && consent.categories) {
consentValue = consent.categories.every(
consentCategory => safeConsent[consentCategory],
);
}

return {
...result,
[consentKey]: consentValue,
};
},
{},
);
const consentValues = Object.keys(
this.googleConsentConfigWithoutMode,
).reduce((result, consentKey) => {
let consentValue = false;

const consent = this.googleConsentConfigWithoutMode[consentKey];

if (consent && consent.categories) {
consentValue = consent.categories.every(
consentCategory => safeConsent[consentCategory],
);
}

return {
...result,
[consentKey]: consentValue,
};
}, {});

await firebaseAnalytics().setConsent(consentValues);

// If in basic mode we need to activate analytics collection
// We do not need to look for the consent variable because this method
// will only be called if the integration was loaded, i.e., the
// statistics consent was given.
if (
get(this.options, `${OPTION_GOOGLE_CONSENT_CONFIG}.mode`) === 'Basic'
) {
await firebaseAnalytics().setAnalyticsCollectionEnabled(true);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const mockFirebaseAnalyticsReturn = {
logEvent: jest.fn(),
logSelectItem: jest.fn(),
setConsent: jest.fn(),
setAnalyticsCollectionEnabled: jest.fn(),
};

jest.mock('@react-native-firebase/analytics', () => {
Expand Down Expand Up @@ -70,18 +71,39 @@ describe('FirebaseAnalyticsIntegration integration', () => {
expect(FirebaseAnalyticsIntegration.prototype).toBeInstanceOf(Integration);
});

it('`shouldLoad` should return false if there is no user consent', () => {
expect(FirebaseAnalyticsIntegration.shouldLoad({ statistics: false })).toBe(
false,
);
expect(FirebaseAnalyticsIntegration.shouldLoad()).toBe(false);
expect(FirebaseAnalyticsIntegration.shouldLoad({})).toBe(false);
});
describe('shouldLoad', () => {
describe('When in basic mode', () => {
it('`shouldLoad` should return false if there is no user consent', () => {
expect(
FirebaseAnalyticsIntegration.shouldLoad({ statistics: false }),
).toBe(false);
expect(FirebaseAnalyticsIntegration.shouldLoad()).toBe(false);
expect(FirebaseAnalyticsIntegration.shouldLoad({})).toBe(false);
});

it('`shouldLoad` should return true if there is user consent', () => {
expect(FirebaseAnalyticsIntegration.shouldLoad({ statistics: true })).toBe(
true,
);
it('`shouldLoad` should return true if there is user consent', () => {
expect(
FirebaseAnalyticsIntegration.shouldLoad({ statistics: true }),
).toBe(true);
});
});

describe('When in advanced mode', () => {
it('`shouldLoad` should return true whether there is user consent or not', () => {
expect(
FirebaseAnalyticsIntegration.shouldLoad(
{ statistics: false },
{ [OPTION_GOOGLE_CONSENT_CONFIG]: { mode: 'Advanced' } },
),
).toBe(true);
expect(
FirebaseAnalyticsIntegration.shouldLoad(
{ statistics: true },
{ [OPTION_GOOGLE_CONSENT_CONFIG]: { mode: 'Advanced' } },
),
).toBe(true);
});
});
});

it('Should return a FirebaseAnalyticsIntegration instance from createInstance', () => {
Expand Down Expand Up @@ -986,6 +1008,60 @@ describe('FirebaseAnalyticsIntegration instance', () => {
});

describe('Consent', () => {
describe('When in basic mode', () => {
it('should call `setAnalyticsCollectionEnabled` with true in setConsent', async () => {
const instance = createInstance({
[OPTION_GOOGLE_CONSENT_CONFIG]: {
ad_user_data: { categories: ['marketing'] },
ad_personalization: { categories: ['marketing'] },
analytics_storage: { categories: ['marketing'] },
ad_storage: { categories: ['marketing'] },
mode: 'Basic',
},
});

await instance.setConsent({ marketing: true });

expect(firebaseAnalytics().setConsent).toHaveBeenCalledWith({
ad_personalization: true,
ad_storage: true,
ad_user_data: true,
analytics_storage: true,
});

expect(
firebaseAnalytics().setAnalyticsCollectionEnabled,
).toHaveBeenCalledWith(true);
});
});

describe('When in advanced mode', () => {
it('should not call `setAnalyticsCollectionEnabled` in setConsent', async () => {
const instance = createInstance({
[OPTION_GOOGLE_CONSENT_CONFIG]: {
ad_user_data: { categories: ['marketing'] },
ad_personalization: { categories: ['marketing'] },
analytics_storage: { categories: ['marketing'] },
ad_storage: { categories: ['marketing'] },
mode: 'Advanced',
},
});

await instance.setConsent({ marketing: true });

expect(firebaseAnalytics().setConsent).toHaveBeenCalledWith({
ad_personalization: true,
ad_storage: true,
ad_user_data: true,
analytics_storage: true,
});

expect(
firebaseAnalytics().setAnalyticsCollectionEnabled,
).not.toHaveBeenCalled();
});
});

it('Should update the user consent on the native side when setConsent is called', async () => {
const instance = createInstance({
[OPTION_GOOGLE_CONSENT_CONFIG]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ describe('Forter', () => {
}),
);

jest.clearAllMocks();

const mockSecondUserEventData = generateAnalyticsEventData(
'onSetUser',
'onSetUser',
Expand Down
Loading
Loading