@react-native-firebase/analytics 20.1.0 → 20.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }