@react-native-firebase/analytics
Version:
React Native Firebase - The analytics module provides out of the box support with Google Analytics for Firebase. Integration with the Android & iOS allows for in-depth analytical insight reporting, such as device information, location, user actions and mo
333 lines (304 loc) • 10.1 kB
JavaScript
/* eslint-disable no-console */
import {
getApp,
getId,
onIdChange,
getInstallations,
makeIDBAvailable,
} from '@react-native-firebase/app/lib/internal/web/firebaseInstallations';
import {
getItem,
setItem,
isMemoryStorage,
} from '@react-native-firebase/app/lib/internal/asyncStorage';
import { isNumber } from '@react-native-firebase/app/lib/common';
/**
* Generates a Google Analytics client ID.
* @returns {string} The generated client ID.
*/
function generateGAClientId() {
const randomNumber = Math.round(Math.random() * 2147483647);
// TODO: Don't seem to need this for now.
// var hash = 1;
// if (seed) {
// for (var i = seed.length - 1; i >= 0; i--) {
// var char = seed.charCodeAt(i);
// hash = ((hash << 6) & 268435455) + char + (char << 14);
// var flag = hash & 266338304;
// hash = flag !== 0 ? hash ^ (flag >> 21) : hash;
// }
// }
// const randomPart = seed ? String(randomNumber ^ (hash & 2147483647)) : String(randomNumber);
const randomPart = String(randomNumber);
const timestamp = Math.round(Date.now() / 1000);
return randomPart + '.' + timestamp;
}
class AnalyticsApi {
constructor(appName, measurementId) {
this.appName = appName;
this.measurementId = measurementId;
this.eventQueue = [];
this.queueTimer = null;
this.queueInterval = 250;
this.defaultEventParameters = {};
this.userId = null;
this.userProperties = {};
this.consent = {};
this.analyticsCollectionEnabled = true;
this.started = false;
this.installationId = null;
this.debug = false;
this.currentScreen = null;
this._getInstallationId().catch(error => {
if (globalThis.RNFBDebug) {
console.debug('[RNFB->Analytics][🔴] Error getting Firebase Installation Id:', error);
} else {
// No-op. This is a non-critical error.
}
});
}
setDefaultEventParameters(params) {
if (params === null || params === undefined) {
this.defaultEventParameters = {};
} else {
for (const [key, value] of Object.entries(params)) {
this.defaultEventParameters[key] = value;
if (value === null) {
delete this.defaultEventParameters[key];
}
}
}
}
setDebug(enabled) {
this.debug = enabled;
}
setUserId(userId) {
this.userId = userId;
}
setCurrentScreen(screenName) {
this.currentScreen = screenName;
}
setUserProperty(key, value) {
this.userProperties[key] = value;
if (value === null) {
delete this.userProperties[key];
}
}
setUserProperties(properties) {
for (const [key, value] of Object.entries(properties)) {
this.setUserProperty(key, value);
}
}
setConsent(consentSettings) {
this.consent = { ...this.consent, ...consentSettings };
}
setAnalyticsCollectionEnabled(enabled) {
this.analyticsCollectionEnabled = enabled;
if (!enabled) {
this._stopQueueProcessing();
} else {
this._startQueueProcessing();
}
}
logEvent(eventName, eventParams = {}) {
if (!this.analyticsCollectionEnabled) return;
this.eventQueue.push({
name: eventName,
params: { ...this.defaultEventParameters, ...eventParams },
});
this._startQueueProcessing();
}
async _getInstallationId() {
navigator.onLine = true;
makeIDBAvailable();
const app = getApp(this.appName);
const installations = getInstallations(app);
const id = await getId(installations);
if (globalThis.RNFBDebug) {
console.debug('[RNFB->Analytics][📊] Firebase Installation Id:', id);
}
this.installationId = id;
onIdChange(installations, newId => {
this.installationId = newId;
});
}
_startQueueProcessing() {
if (this.started) return;
this.sessionId = Math.floor(Date.now() / 1000);
this.started = true;
this.queueTimer = setInterval(
() => this._processQueue().catch(console.error),
this.queueInterval,
);
}
_stopQueueProcessing() {
if (!this.started) return;
this.started = false;
clearInterval(this.queueTimer);
}
async _processQueue() {
if (this.eventQueue.length === 0) return;
const events = this.eventQueue.splice(0, 5);
await this._sendEvents(events);
if (this.eventQueue.length === 0) {
this._stopQueueProcessing();
}
}
async _getCid() {
this.cid = await getItem('analytics:cid');
if (this.cid) {
return this.cid;
}
this.cid = generateGAClientId();
await setItem('analytics:cid', this.cid);
if (isMemoryStorage()) {
console.warn(
'Firebase Analytics is using in memory persistence. This means that the analytics\n' +
'client ID is reset every time your app is restarted which may result in\n' +
'inaccurate data being shown on the Firebase Analytics dashboard.\n' +
'\n' +
'To enable persistence, provide an Async Storage implementation.\n' +
'\n' +
'For example, to use React Native Async Storage:\n' +
'\n' +
" import AsyncStorage from '@react-native-async-storage/async-storage';\n" +
'\n' +
' // Before initializing Firebase set the Async Storage implementation\n' +
' // that will be used to persist user sessions.\n' +
' firebase.setReactNativeAsyncStorage(AsyncStorage);\n' +
'\n' +
' // Then initialize Firebase as normal.\n' +
' await firebase.initializeApp({ ... });\n',
);
}
return this.cid;
}
async _sendEvents(events) {
const cid = this.cid || (await this._getCid());
for (const event of events) {
const queryParams = new URLSearchParams({
v: '2',
tid: this.measurementId,
en: event.name,
cid,
pscdl: 'noapi',
sid: this.sessionId,
'ep.origin': 'firebase',
_z: 'fetch',
_p: '' + Date.now(),
_s: 1,
_ee: 1,
dma: 0,
tfd: Math.round(performance.now()),
are: 1,
sct: 2,
seg: 1,
frm: 0,
});
if (this.debug) {
queryParams.append('_dbg', '1');
queryParams.append('ep.debug_mode', '1');
}
if (this.consent && !this.consent.ad_personalization) {
queryParams.append('npa', '1');
} else {
queryParams.append('npa', '0');
}
if (this.userId) {
queryParams.append('uid', this.userId);
}
if (this.installationId) {
queryParams.append('_fid', this.installationId);
}
if (this.userProperties && Object.keys(this.userProperties).length > 0) {
for (const [key, value] of Object.entries(this.userProperties)) {
queryParams.append(`up.${key}`, `${value}`);
}
}
if (this.currentScreen) {
queryParams.append('ep.screen_name', this.currentScreen);
queryParams.append('ep.firebase_screen', this.currentScreen);
}
if (event.params && Object.keys(event.params).length > 0) {
// TODO we need to handle 'items' arrays and also key name conversions based on the following map;
// const keyConvert = {
// item_id: 'id',
// item_name: 'nm',
// item_brand: 'br',
// item_category: 'ca',
// item_category2: 'c2',
// item_category3: 'c3',
// item_category4: 'c4',
// item_category5: 'c5',
// item_variant: 'va',
// price: 'pr',
// quantity: 'qt',
// coupon: 'cp',
// item_list_name: 'ln',
// index: 'lp',
// item_list_id: 'li',
// discount: 'ds',
// affiliation: 'af',
// promotion_id: 'pi',
// promotion_name: 'pn',
// creative_name: 'cn',
// creative_slot: 'cs',
// location_id: 'lo',
// id: 'id',
// name: 'nm',
// brand: 'br',
// variant: 'va',
// list_name: 'ln',
// list_position: 'lp',
// list: 'ln',
// position: 'lp',
// creative: 'cn',
// };
// items array should for example become:
// pr1 for items[0]
// pr2 for items[1]
// ... etc
// with the format for each looking something like:
// iditem_id~nmitem_name~britem_brand~caitem_category~c2item_category2~c3item_category3~c4item_category4~c5item_category5~vaitem_variant~prprice~qtquantity~cpcoupon~lnitem_list_name~lpindex~liitem_list_id~dsdiscount~afaffiliation~pipromotion_id~pnpromotion_name~cncreative_name~cscreative_slot~lolocation_id
for (const [key, value] of Object.entries(event.params)) {
if (isNumber(value)) {
queryParams.append(`epn.${key}`, `${value}`);
} else {
queryParams.append(`ep.${key}`, `${value}`);
}
}
}
try {
const url = `https://www.google-analytics.com/g/collect?${queryParams.toString()}`;
if (globalThis.RNFBDebug) {
console.debug(`[RNFB-->Fetch][📊] Sending analytics call: ${url}`);
}
const response = await fetch(url, {
method: 'POST',
mode: 'no-cors',
headers: {
accept: '*/*',
'accept-encoding': 'gzip, deflate, br',
'Content-Type': 'text/plain;charset=UTF-8',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'no-cache',
'content-length': '0',
origin: 'firebase',
pragma: 'no-cache',
'sec-fetch-dest': 'empty',
'sec-fetch-site': 'cross-site',
'user-agent': 'react-native-firebase',
},
});
if (globalThis.RNFBDebug) {
console.debug(`[RNFB<--Fetch][📊] Response: ${response.status}`);
}
} catch (error) {
if (globalThis.RNFBDebug) {
console.debug('[RNFB<--Fetch][🔴] Error sending Analytics event:', error);
}
}
}
}
}
export { AnalyticsApi };