@react-native-firebase/analytics 20.1.0 → 20.2.0

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [20.2.0](https://github.com/invertase/react-native-firebase/compare/v20.1.0...v20.2.0) (2024-07-15)
7
+
8
+ ### Features
9
+
10
+ - **other:** Add analytics support ([#7899](https://github.com/invertase/react-native-firebase/issues/7899)) ([cbdf9ec](https://github.com/invertase/react-native-firebase/commit/cbdf9ec78452a73751d1afeca428842898f63134))
11
+
6
12
  ## [20.1.0](https://github.com/invertase/react-native-firebase/compare/v20.0.0...v20.1.0) (2024-06-04)
7
13
 
8
14
  **Note:** Version bump only for package @react-native-firebase/analytics
package/lib/index.js CHANGED
@@ -33,8 +33,10 @@ import {
33
33
  FirebaseModule,
34
34
  getFirebaseRoot,
35
35
  } from '@react-native-firebase/app/lib/internal';
36
+ import { setReactNativeModule } from '@react-native-firebase/app/lib/internal/nativeModule';
36
37
  import { isBoolean } from '@react-native-firebase/app/lib/common';
37
38
 
39
+ import fallBackModule from './web/RNFBAnalyticsModule';
38
40
  import version from './version';
39
41
  import * as structs from './structs';
40
42
 
@@ -828,3 +830,6 @@ export default createModuleNamespace({
828
830
  // analytics().logEvent(...);
829
831
  // firebase.analytics().logEvent(...);
830
832
  export const firebase = getFirebaseRoot();
833
+
834
+ // Register the interop module for non-native platforms.
835
+ setReactNativeModule(nativeModuleName, fallBackModule);
package/lib/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- module.exports = '20.1.0';
2
+ module.exports = '20.2.0';
@@ -0,0 +1,2 @@
1
+ // No-op for android.
2
+ export default {};
@@ -0,0 +1,2 @@
1
+ // No-op for ios.
2
+ export default {};
@@ -0,0 +1,119 @@
1
+ import { getApp } from '@react-native-firebase/app/lib/internal/web/firebaseApp';
2
+ import { guard } from '@react-native-firebase/app/lib/internal/web/utils';
3
+
4
+ import { AnalyticsApi } from './api';
5
+
6
+ let analyticsInstances = {};
7
+
8
+ function getAnalyticsApi() {
9
+ const app = getApp('[DEFAULT]');
10
+ const measurementId = app.options.measurementId;
11
+ if (!measurementId) {
12
+ // eslint-disable-next-line no-console
13
+ console.warn(
14
+ 'No measurement id found for Firebase Analytics. Analytics will be unavailable. Have you set gaTrackingId in your Firebase config?',
15
+ );
16
+ }
17
+ if (!analyticsInstances[measurementId]) {
18
+ analyticsInstances[measurementId] = new AnalyticsApi('[DEFAULT]', measurementId);
19
+ if (global.RNFBDebug) {
20
+ analyticsInstances[measurementId].setDebug(true);
21
+ }
22
+ }
23
+ return analyticsInstances[measurementId];
24
+ }
25
+
26
+ /**
27
+ * This is a 'NativeModule' for the web platform.
28
+ * Methods here are identical to the ones found in
29
+ * the native android/ios modules e.g. `@ReactMethod` annotated
30
+ * java methods on Android.
31
+ */
32
+ export default {
33
+ logEvent(name, params) {
34
+ return guard(async () => {
35
+ const api = getAnalyticsApi('[DEFAULT]');
36
+ api.logEvent(name, params);
37
+ return null;
38
+ });
39
+ },
40
+
41
+ setUserId(userId) {
42
+ return guard(async () => {
43
+ const api = getAnalyticsApi('[DEFAULT]');
44
+ api.setUserId(userId);
45
+ return null;
46
+ });
47
+ },
48
+
49
+ setUserProperty(key, value) {
50
+ return guard(async () => {
51
+ const api = getAnalyticsApi('[DEFAULT]');
52
+ api.setUserProperty(key, value);
53
+ return null;
54
+ });
55
+ },
56
+
57
+ setUserProperties(properties) {
58
+ return guard(async () => {
59
+ const api = getAnalyticsApi('[DEFAULT]');
60
+ api.setUserProperties(properties);
61
+ return null;
62
+ });
63
+ },
64
+
65
+ setDefaultEventParameters(params) {
66
+ return guard(async () => {
67
+ const api = getAnalyticsApi('[DEFAULT]');
68
+ api.setDefaultEventParameters(params);
69
+ return null;
70
+ });
71
+ },
72
+
73
+ setConsent(consent) {
74
+ return guard(async () => {
75
+ const api = getAnalyticsApi('[DEFAULT]');
76
+ // TODO currently we only support ad_personalization
77
+ if (consent && consent.ad_personalization) {
78
+ api.setConsent({ ad_personalization: consent.ad_personalization });
79
+ }
80
+ return null;
81
+ });
82
+ },
83
+
84
+ setAnalyticsCollectionEnabled(enabled) {
85
+ return guard(async () => {
86
+ const api = getAnalyticsApi('[DEFAULT]');
87
+ api.setAnalyticsCollectionEnabled(enabled);
88
+ return null;
89
+ });
90
+ },
91
+
92
+ resetAnalyticsData() {
93
+ // Unsupported for web.
94
+ return guard(async () => {
95
+ return null;
96
+ });
97
+ },
98
+
99
+ setSessionTimeoutDuration() {
100
+ // Unsupported for web.
101
+ return guard(async () => {
102
+ return null;
103
+ });
104
+ },
105
+
106
+ getAppInstanceId() {
107
+ // Unsupported for web.
108
+ return guard(async () => {
109
+ return null;
110
+ });
111
+ },
112
+
113
+ getSessionId() {
114
+ // Unsupported for web.
115
+ return guard(async () => {
116
+ return null;
117
+ });
118
+ },
119
+ };
package/lib/web/api.js ADDED
@@ -0,0 +1,297 @@
1
+ /* eslint-disable no-console */
2
+ import {
3
+ getApp,
4
+ getId,
5
+ onIdChange,
6
+ getInstallations,
7
+ makeIDBAvailable,
8
+ } from '@react-native-firebase/app/lib/internal/web/firebaseInstallations';
9
+ import { isNumber } from '@react-native-firebase/app/lib/common';
10
+
11
+ /**
12
+ * Generates a Google Analytics client ID.
13
+ * @returns {string} The generated client ID.
14
+ */
15
+ function generateGAClientId() {
16
+ const randomNumber = Math.round(Math.random() * 2147483647);
17
+ // TODO: Don't seem to need this for now.
18
+ // var hash = 1;
19
+ // if (seed) {
20
+ // for (var i = seed.length - 1; i >= 0; i--) {
21
+ // var char = seed.charCodeAt(i);
22
+ // hash = ((hash << 6) & 268435455) + char + (char << 14);
23
+ // var flag = hash & 266338304;
24
+ // hash = flag !== 0 ? hash ^ (flag >> 21) : hash;
25
+ // }
26
+ // }
27
+ // const randomPart = seed ? String(randomNumber ^ (hash & 2147483647)) : String(randomNumber);
28
+ const randomPart = String(randomNumber);
29
+ const timestamp = Math.round(Date.now() / 1000);
30
+ return randomPart + '.' + timestamp;
31
+ }
32
+
33
+ class AnalyticsApi {
34
+ constructor(appName, measurementId) {
35
+ this.appName = appName;
36
+ this.measurementId = measurementId;
37
+ this.eventQueue = [];
38
+ this.queueTimer = null;
39
+ this.queueInterval = 250;
40
+ this.defaultEventParameters = {};
41
+ this.userId = null;
42
+ this.userProperties = {};
43
+ this.consent = {};
44
+ this.analyticsCollectionEnabled = true;
45
+ this.started = false;
46
+ this.installationId = null;
47
+ this.debug = false;
48
+ this.currentScreen = null;
49
+
50
+ // TODO this should be persisted once we have a way to do so in app internals
51
+ this.cid = generateGAClientId();
52
+ this._getInstallationId().catch(error => {
53
+ if (global.RNFBDebug) {
54
+ console.debug('[RNFB->Analytics][🔴] Error getting Firebase Installation Id:', error);
55
+ } else {
56
+ // No-op. This is a non-critical error.
57
+ }
58
+ });
59
+ }
60
+
61
+ setDefaultEventParameters(params) {
62
+ if (params === null || params === undefined) {
63
+ this.defaultEventParameters = {};
64
+ } else {
65
+ for (const [key, value] of Object.entries(params)) {
66
+ this.defaultEventParameters[key] = value;
67
+ if (value === null) {
68
+ delete this.defaultEventParameters[key];
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ setDebug(enabled) {
75
+ this.debug = enabled;
76
+ }
77
+
78
+ setUserId(userId) {
79
+ this.userId = userId;
80
+ }
81
+
82
+ setCurrentScreen(screenName) {
83
+ this.currentScreen = screenName;
84
+ }
85
+
86
+ setUserProperty(key, value) {
87
+ this.userProperties[key] = value;
88
+ if (value === null) {
89
+ delete this.userProperties[key];
90
+ }
91
+ }
92
+
93
+ setUserProperties(properties) {
94
+ for (const [key, value] of Object.entries(properties)) {
95
+ this.setUserProperty(key, value);
96
+ }
97
+ }
98
+
99
+ setConsent(consentSettings) {
100
+ this.consent = { ...this.consent, ...consentSettings };
101
+ }
102
+
103
+ setAnalyticsCollectionEnabled(enabled) {
104
+ this.analyticsCollectionEnabled = enabled;
105
+ if (!enabled) {
106
+ this._stopQueueProcessing();
107
+ } else {
108
+ this._startQueueProcessing();
109
+ }
110
+ }
111
+
112
+ logEvent(eventName, eventParams = {}) {
113
+ if (!this.analyticsCollectionEnabled) return;
114
+ this.eventQueue.push({
115
+ name: eventName,
116
+ params: { ...this.defaultEventParameters, ...eventParams },
117
+ });
118
+ this._startQueueProcessing();
119
+ }
120
+
121
+ async _getInstallationId() {
122
+ navigator.onLine = true;
123
+ makeIDBAvailable();
124
+ const app = getApp(this.appName);
125
+ const installations = getInstallations(app);
126
+ const id = await getId(installations);
127
+ if (global.RNFBDebug) {
128
+ console.debug('[RNFB->Analytics][📊] Firebase Installation Id:', id);
129
+ }
130
+ this.installationId = id;
131
+ onIdChange(installations, newId => {
132
+ this.installationId = newId;
133
+ });
134
+ }
135
+
136
+ _startQueueProcessing() {
137
+ if (this.started) return;
138
+ this.sessionId = Math.floor(Date.now() / 1000);
139
+ this.started = true;
140
+ this.queueTimer = setInterval(
141
+ () => this._processQueue().catch(console.error),
142
+ this.queueInterval,
143
+ );
144
+ }
145
+
146
+ _stopQueueProcessing() {
147
+ if (!this.started) return;
148
+ this.started = false;
149
+ clearInterval(this.queueTimer);
150
+ }
151
+
152
+ async _processQueue() {
153
+ if (this.eventQueue.length === 0) return;
154
+ const events = this.eventQueue.splice(0, 5);
155
+ await this._sendEvents(events);
156
+ if (this.eventQueue.length === 0) {
157
+ this._stopQueueProcessing();
158
+ }
159
+ }
160
+
161
+ async _sendEvents(events) {
162
+ for (const event of events) {
163
+ const queryParams = new URLSearchParams({
164
+ v: '2',
165
+ tid: this.measurementId,
166
+ en: event.name,
167
+ cid: this.cid,
168
+ pscdl: 'noapi',
169
+ sid: this.sessionId,
170
+ 'ep.origin': 'firebase',
171
+ _z: 'fetch',
172
+ _p: '' + Date.now(),
173
+ _s: 1,
174
+ _ee: 1,
175
+ dma: 0,
176
+ tfd: Math.round(performance.now()),
177
+ are: 1,
178
+ sct: 2,
179
+ seg: 1,
180
+ frm: 0,
181
+ });
182
+
183
+ if (this.debug) {
184
+ queryParams.append('_dbg', '1');
185
+ queryParams.append('ep.debug_mode', '1');
186
+ }
187
+
188
+ if (this.consent && !this.consent.ad_personalization) {
189
+ queryParams.append('npa', '1');
190
+ } else {
191
+ queryParams.append('npa', '0');
192
+ }
193
+
194
+ if (this.userId) {
195
+ queryParams.append('uid', this.userId);
196
+ }
197
+
198
+ if (this.installationId) {
199
+ queryParams.append('_fid', this.installationId);
200
+ }
201
+
202
+ if (this.userProperties && Object.keys(this.userProperties).length > 0) {
203
+ for (const [key, value] of Object.entries(this.userProperties)) {
204
+ queryParams.append(`up.${key}`, `${value}`);
205
+ }
206
+ }
207
+
208
+ if (this.currentScreen) {
209
+ queryParams.append('ep.screen_name', this.currentScreen);
210
+ queryParams.append('ep.firebase_screen', this.currentScreen);
211
+ }
212
+
213
+ if (event.params && Object.keys(event.params).length > 0) {
214
+ // TODO we need to handle 'items' arrays and also key name conversions based on the following map;
215
+ // const keyConvert = {
216
+ // item_id: 'id',
217
+ // item_name: 'nm',
218
+ // item_brand: 'br',
219
+ // item_category: 'ca',
220
+ // item_category2: 'c2',
221
+ // item_category3: 'c3',
222
+ // item_category4: 'c4',
223
+ // item_category5: 'c5',
224
+ // item_variant: 'va',
225
+ // price: 'pr',
226
+ // quantity: 'qt',
227
+ // coupon: 'cp',
228
+ // item_list_name: 'ln',
229
+ // index: 'lp',
230
+ // item_list_id: 'li',
231
+ // discount: 'ds',
232
+ // affiliation: 'af',
233
+ // promotion_id: 'pi',
234
+ // promotion_name: 'pn',
235
+ // creative_name: 'cn',
236
+ // creative_slot: 'cs',
237
+ // location_id: 'lo',
238
+ // id: 'id',
239
+ // name: 'nm',
240
+ // brand: 'br',
241
+ // variant: 'va',
242
+ // list_name: 'ln',
243
+ // list_position: 'lp',
244
+ // list: 'ln',
245
+ // position: 'lp',
246
+ // creative: 'cn',
247
+ // };
248
+ // items array should for example become:
249
+ // pr1 for items[0]
250
+ // pr2 for items[1]
251
+ // ... etc
252
+ // with the format for each looking something like:
253
+ // 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
254
+ for (const [key, value] of Object.entries(event.params)) {
255
+ if (isNumber(value)) {
256
+ queryParams.append(`epn.${key}`, `${value}`);
257
+ } else {
258
+ queryParams.append(`ep.${key}`, `${value}`);
259
+ }
260
+ }
261
+ }
262
+
263
+ try {
264
+ const url = `https://www.google-analytics.com/g/collect?${queryParams.toString()}`;
265
+ if (global.RNFBDebug) {
266
+ console.debug(`[RNFB-->Fetch][📊] Sending analytics call: ${url}`);
267
+ }
268
+ const response = await fetch(url, {
269
+ method: 'POST',
270
+ headers: {
271
+ accept: '*/*',
272
+ 'accept-encoding': 'gzip, deflate, br',
273
+ 'Content-Type': 'text/plain;charset=UTF-8',
274
+ 'accept-language': 'en-US,en;q=0.9',
275
+ 'cache-control': 'no-cache',
276
+ 'content-length': '0',
277
+ origin: 'firebase',
278
+ pragma: 'no-cache',
279
+ 'sec-fetch-dest': 'empty',
280
+ 'sec-fetch-mode': 'no-cors',
281
+ 'sec-fetch-site': 'cross-site',
282
+ 'user-agent': 'react-native-firebase',
283
+ },
284
+ });
285
+ if (global.RNFBDebug) {
286
+ console.debug(`[RNFB<--Fetch][📊] Response: ${response.status}`);
287
+ }
288
+ } catch (error) {
289
+ if (global.RNFBDebug) {
290
+ console.debug('[RNFB<--Fetch][🔴] Error sending Analytics event:', error);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ export { AnalyticsApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-firebase/analytics",
3
- "version": "20.1.0",
3
+ "version": "20.2.0",
4
4
  "author": "Invertase <oss@invertase.io> (http://invertase.io)",
5
5
  "description": "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 more.",
6
6
  "main": "lib/index.js",
@@ -22,10 +22,10 @@
22
22
  "analytics"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@react-native-firebase/app": "20.1.0"
25
+ "@react-native-firebase/app": "20.2.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
- "gitHead": "99819b9cef23ac46080cbffe50cc85083305d04c"
30
+ "gitHead": "82c50138d07e673213cd8dee5ce9a2f9b5656649"
31
31
  }