@react-native-firebase/analytics 26.3.3 → 27.0.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/RNFBAnalytics.podspec +49 -2
  3. package/android/src/main/java/io/invertase/firebase/analytics/UniversalFirebaseAnalyticsModule.java +17 -15
  4. package/android/src/reactnative/java/io/invertase/firebase/analytics/NativeRNFBTurboAnalytics.java +4 -3
  5. package/dist/module/index.js +26 -11
  6. package/dist/module/index.js.map +1 -1
  7. package/dist/module/version.js +1 -1
  8. package/dist/module/web/api.js +42 -19
  9. package/dist/module/web/api.js.map +1 -1
  10. package/dist/typescript/lib/index.d.ts +2 -2
  11. package/dist/typescript/lib/index.d.ts.map +1 -1
  12. package/dist/typescript/lib/version.d.ts +1 -1
  13. package/dist/typescript/lib/web/api.d.ts +4 -1
  14. package/dist/typescript/lib/web/api.d.ts.map +1 -1
  15. package/ios/RNFBAnalytics/RNFBAnalyticsConsentSettingsMapper.swift +98 -0
  16. package/ios/RNFBAnalytics/RNFBAnalyticsHelper.h +111 -0
  17. package/ios/RNFBAnalytics/RNFBAnalyticsHelper.m +338 -0
  18. package/ios/RNFBAnalytics/RNFBAnalyticsJavascriptParamsCleaner.swift +167 -0
  19. package/ios/RNFBAnalytics/RNFBAnalyticsLogTransaction.swift +2 -2
  20. package/ios/RNFBAnalytics/RNFBAnalyticsModule.mm +32 -251
  21. package/ios/RNFBAnalytics.xcodeproj/project.pbxproj +16 -2
  22. package/ios/RNFBAnalyticsUnitTests/HostStubs/Firebase/FIRAnalyticsHostStub.m +304 -0
  23. package/ios/RNFBAnalyticsUnitTests/HostStubs/Firebase/Firebase.h +71 -0
  24. package/ios/RNFBAnalyticsUnitTests/HostStubs/RNFBAnalytics/RNFBAnalyticsLogTransactionHostStub.h +20 -0
  25. package/ios/RNFBAnalyticsUnitTests/HostStubs/RNFBAnalytics/RNFBAnalyticsLogTransactionHostStub.m +43 -0
  26. package/ios/RNFBAnalyticsUnitTests/HostStubs/RNFBApp/RNFBSharedUtils.h +21 -0
  27. package/ios/RNFBAnalyticsUnitTests/HostStubs/RNFBApp/RNFBSharedUtilsHostStub.m +26 -0
  28. package/ios/RNFBAnalyticsUnitTests/HostStubs/React/RCTBridgeModule.h +10 -0
  29. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsConsentSettingsMapperHostStubTests.m +62 -0
  30. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsConsentSettingsMapperTests.swift +91 -0
  31. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsHelperHostStubTests.m +542 -0
  32. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsJavascriptParamsCleanerHostStubTests.m +59 -0
  33. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsJavascriptParamsCleanerTests.swift +205 -0
  34. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsNullSentinelDecodeTests.m +111 -0
  35. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsUnitTests.xcodeproj/project.pbxproj +302 -0
  36. package/ios/RNFBAnalyticsUnitTests/RNFBAnalyticsUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBAnalyticsUnitTests.xcscheme +69 -0
  37. package/lib/index.ts +38 -10
  38. package/lib/version.ts +1 -1
  39. package/lib/web/api.ts +51 -19
  40. package/package.json +5 -4
@@ -0,0 +1,69 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Scheme
3
+ LastUpgradeVersion = "2600"
4
+ version = "1.7">
5
+ <BuildAction
6
+ parallelizeBuildables = "YES"
7
+ buildImplicitDependencies = "YES">
8
+ <BuildActionEntries>
9
+ <BuildActionEntry
10
+ buildForTesting = "YES"
11
+ buildForRunning = "YES"
12
+ buildForProfiling = "NO"
13
+ buildForArchiving = "NO"
14
+ buildForAnalyzing = "YES">
15
+ <BuildableReference
16
+ BuildableIdentifier = "primary"
17
+ BlueprintIdentifier = "A10000000000000000000002"
18
+ BuildableName = "RNFBAnalyticsUnitTests.xctest"
19
+ BlueprintName = "RNFBAnalyticsUnitTests"
20
+ ReferencedContainer = "container:RNFBAnalyticsUnitTests.xcodeproj">
21
+ </BuildableReference>
22
+ </BuildActionEntry>
23
+ </BuildActionEntries>
24
+ </BuildAction>
25
+ <TestAction
26
+ buildConfiguration = "Debug"
27
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29
+ shouldUseLaunchSchemeArgsEnv = "YES"
30
+ codeCoverageEnabled = "YES">
31
+ <Testables>
32
+ <TestableReference
33
+ skipped = "NO">
34
+ <BuildableReference
35
+ BuildableIdentifier = "primary"
36
+ BlueprintIdentifier = "A10000000000000000000002"
37
+ BuildableName = "RNFBAnalyticsUnitTests.xctest"
38
+ BlueprintName = "RNFBAnalyticsUnitTests"
39
+ ReferencedContainer = "container:RNFBAnalyticsUnitTests.xcodeproj">
40
+ </BuildableReference>
41
+ </TestableReference>
42
+ </Testables>
43
+ </TestAction>
44
+ <LaunchAction
45
+ buildConfiguration = "Debug"
46
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
47
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
48
+ launchStyle = "0"
49
+ useCustomWorkingDirectory = "NO"
50
+ ignoresPersistentStateOnLaunch = "NO"
51
+ debugDocumentVersioning = "YES"
52
+ debugServiceExtension = "internal"
53
+ allowLocationSimulation = "YES">
54
+ </LaunchAction>
55
+ <ProfileAction
56
+ buildConfiguration = "Release"
57
+ shouldUseLaunchSchemeArgsEnv = "YES"
58
+ savedToolIdentifier = ""
59
+ useCustomWorkingDirectory = "NO"
60
+ debugDocumentVersioning = "YES">
61
+ </ProfileAction>
62
+ <AnalyzeAction
63
+ buildConfiguration = "Debug">
64
+ </AnalyzeAction>
65
+ <ArchiveAction
66
+ buildConfiguration = "Release"
67
+ revealArchiveInOrganizer = "YES">
68
+ </ArchiveAction>
69
+ </Scheme>
package/lib/index.ts CHANGED
@@ -18,10 +18,10 @@
18
18
  import type { FirebaseApp } from '@react-native-firebase/app';
19
19
  import { Platform } from 'react-native';
20
20
  import {
21
- isAlphaNumericUnderscore,
22
21
  isE164PhoneNumber,
23
22
  isIOS,
24
23
  isBoolean,
24
+ isFinite,
25
25
  isNull,
26
26
  isNumber,
27
27
  isObject,
@@ -33,13 +33,13 @@ import {
33
33
  import {
34
34
  FirebaseModule,
35
35
  getOrCreateModularInstance,
36
+ setReactNativeModule,
36
37
  } from '@react-native-firebase/app/dist/module/internal';
37
38
  import type { ModuleConfig } from '@react-native-firebase/app/dist/module/internal';
38
39
 
39
40
  import './types/internal';
40
41
 
41
42
  // Internal types are now available through module declarations in app package
42
- import { setReactNativeModule } from '@react-native-firebase/app/dist/module/internal/nativeModule';
43
43
 
44
44
  import { validateStruct, validateCompound } from './struct';
45
45
  import { RNFBAnalyticsModule } from './web/RNFBAnalyticsModule';
@@ -170,9 +170,13 @@ const ReservedEventNames: readonly string[] = [
170
170
  'user_engagement',
171
171
  ] as const;
172
172
 
173
+ const ReservedEventNamePrefixes = ['firebase_', 'google_', 'ga_'] as const;
174
+ const ValidEventName = /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/;
175
+
173
176
  const namespace = 'analytics';
174
177
 
175
178
  const nativeModuleName = 'NativeRNFBTurboAnalytics' as const;
179
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/i;
176
180
 
177
181
  class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
178
182
  logEvent(
@@ -189,16 +193,22 @@ class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
189
193
  }
190
194
 
191
195
  // check name is not a reserved event name
192
- if (isOneOf(name, ReservedEventNames as any[])) {
196
+ if (ReservedEventNames.includes(name)) {
193
197
  throw new Error(
194
198
  `firebase.analytics().logEvent(*) 'name' the event name '${name}' is reserved and can not be used.`,
195
199
  );
196
200
  }
197
201
 
202
+ if (ReservedEventNamePrefixes.some(prefix => name.startsWith(prefix))) {
203
+ throw new Error(
204
+ `firebase.analytics().logEvent(*) 'name' the event name '${name}' uses a reserved prefix and can not be used.`,
205
+ );
206
+ }
207
+
198
208
  // name format validation
199
- if (!isAlphaNumericUnderscore(name) || name.length > 40) {
209
+ if (!ValidEventName.test(name)) {
200
210
  throw new Error(
201
- `firebase.analytics().logEvent(*) 'name' invalid event name '${name}'. Names should contain 1 to 40 alphanumeric characters or underscores.`,
211
+ `firebase.analytics().logEvent(*) 'name' invalid event name '${name}'. Names must start with an alphabetic character and contain 1 to 40 alphanumeric characters or underscores.`,
202
212
  );
203
213
  }
204
214
 
@@ -234,6 +244,12 @@ class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
234
244
  );
235
245
  }
236
246
 
247
+ if (!isFinite(milliseconds)) {
248
+ throw new Error(
249
+ "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a finite number value.",
250
+ );
251
+ }
252
+
237
253
  if (milliseconds < 0) {
238
254
  throw new Error(
239
255
  "firebase.analytics().setSessionTimeoutDuration(*) 'milliseconds' expected a positive number value.",
@@ -867,6 +883,12 @@ class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
867
883
  );
868
884
  }
869
885
 
886
+ if (!SHA256_HEX_PATTERN.test(hashedEmailAddress)) {
887
+ throw new Error(
888
+ "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedEmailAddress(*) 'hashedEmailAddress' expected a 64-character SHA-256 hex string.",
889
+ );
890
+ }
891
+
870
892
  if (!isIOS) {
871
893
  return Promise.resolve();
872
894
  }
@@ -898,15 +920,21 @@ class FirebaseAnalyticsModule extends FirebaseModule<typeof nativeModuleName> {
898
920
  initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
899
921
  hashedPhoneNumber: string,
900
922
  ): Promise<void> {
923
+ if (!isString(hashedPhoneNumber)) {
924
+ throw new Error(
925
+ "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
926
+ );
927
+ }
928
+
901
929
  if (isE164PhoneNumber(hashedPhoneNumber)) {
902
930
  throw new Error(
903
- "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a sha256-hashed value of a phone number in E.164 format.",
931
+ "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a 64-character SHA-256 hex string of a phone number in E.164 format, not an E.164 number.",
904
932
  );
905
933
  }
906
934
 
907
- if (!isString(hashedPhoneNumber)) {
935
+ if (!SHA256_HEX_PATTERN.test(hashedPhoneNumber)) {
908
936
  throw new Error(
909
- "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a string value.",
937
+ "firebase.analytics().initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(*) 'hashedPhoneNumber' expected a 64-character SHA-256 hex string.",
910
938
  );
911
939
  }
912
940
 
@@ -1830,7 +1858,7 @@ export function initiateOnDeviceConversionMeasurementWithEmailAddress(
1830
1858
  * `logTransaction`, non-iOS platforms do not reject.
1831
1859
  *
1832
1860
  * @param analytics Analytics instance.
1833
- * @param hashedEmailAddress sha256-hashed of normalized email address, properly formatted complete with domain name e.g, 'user@example.com'
1861
+ * @param hashedEmailAddress SHA-256 hash of the normalized email address, encoded as a 64-character hex string.
1834
1862
  */
1835
1863
  export function initiateOnDeviceConversionMeasurementWithHashedEmailAddress(
1836
1864
  analytics: Analytics,
@@ -1867,7 +1895,7 @@ export function initiateOnDeviceConversionMeasurementWithPhoneNumber(
1867
1895
  * `logTransaction`, non-iOS platforms do not reject.
1868
1896
  *
1869
1897
  * @param analytics Analytics instance.
1870
- * @param hashedPhoneNumber sha256-hashed of normalized phone number in E.164 format - that is a leading + sign, then up to 15 digits, no dashes or spaces.
1898
+ * @param hashedPhoneNumber SHA-256 hash of the normalized E.164 phone number, encoded as a 64-character hex string.
1871
1899
  */
1872
1900
  export function initiateOnDeviceConversionMeasurementWithHashedPhoneNumber(
1873
1901
  analytics: Analytics,
package/lib/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '26.3.3';
2
+ export const version = '27.0.0';
package/lib/web/api.ts CHANGED
@@ -45,8 +45,11 @@ function generateGAClientId(): string {
45
45
  interface AnalyticsEvent {
46
46
  name: string;
47
47
  params: AnalyticsEventParameters;
48
+ sessionId: number;
48
49
  }
49
50
 
51
+ const DEFAULT_SESSION_TIMEOUT_MS = 1_800_000;
52
+
50
53
  class AnalyticsApi implements IAnalyticsApi {
51
54
  public readonly appName: string;
52
55
  public readonly measurementId: string;
@@ -58,12 +61,14 @@ class AnalyticsApi implements IAnalyticsApi {
58
61
  private userProperties: AnalyticsUserProperties;
59
62
  private consent: AnalyticsConsent;
60
63
  private analyticsCollectionEnabled: boolean;
61
- private started: boolean;
64
+ private processingQueue: boolean;
62
65
  private installationId: string | null;
63
66
  private debug: boolean;
64
67
  private currentScreen: string | null;
65
68
  private sessionId?: number;
69
+ private lastEventTime?: number;
66
70
  private cid?: string | null;
71
+ private cidPromise?: Promise<string>;
67
72
 
68
73
  constructor(appName: string, measurementId: string) {
69
74
  this.appName = appName;
@@ -76,7 +81,7 @@ class AnalyticsApi implements IAnalyticsApi {
76
81
  this.userProperties = {};
77
82
  this.consent = {};
78
83
  this.analyticsCollectionEnabled = true;
79
- this.started = false;
84
+ this.processingQueue = false;
80
85
  this.installationId = null;
81
86
  this.debug = false;
82
87
  this.currentScreen = null;
@@ -143,18 +148,31 @@ class AnalyticsApi implements IAnalyticsApi {
143
148
 
144
149
  logEvent(eventName: string, eventParams: AnalyticsEventParameters = {}): void {
145
150
  if (!this.analyticsCollectionEnabled) return;
151
+ const eventTime = Date.now();
152
+ if (
153
+ this.sessionId === undefined ||
154
+ this.lastEventTime === undefined ||
155
+ eventTime - this.lastEventTime >= DEFAULT_SESSION_TIMEOUT_MS
156
+ ) {
157
+ this.sessionId = Math.floor(eventTime / 1000);
158
+ }
159
+ this.lastEventTime = eventTime;
146
160
  this.eventQueue.push({
147
161
  name: eventName,
148
162
  params: { ...this.defaultEventParameters, ...eventParams },
163
+ sessionId: this.sessionId,
149
164
  });
150
165
  this._startQueueProcessing();
151
166
  }
152
167
 
153
168
  private async _getInstallationId(): Promise<void> {
154
- // @ts-ignore
155
- if (navigator !== null) {
156
- // @ts-ignore
157
- (navigator as any).onLine = true;
169
+ const navigatorObject = globalThis.navigator;
170
+ if (navigatorObject && !('onLine' in navigatorObject) && Object.isExtensible(navigatorObject)) {
171
+ Object.defineProperty(navigatorObject, 'onLine', {
172
+ configurable: true,
173
+ value: true,
174
+ writable: true,
175
+ });
158
176
  }
159
177
  makeIDBAvailable();
160
178
  const app = getApp(this.appName);
@@ -170,9 +188,7 @@ class AnalyticsApi implements IAnalyticsApi {
170
188
  }
171
189
 
172
190
  private _startQueueProcessing(): void {
173
- if (this.started) return;
174
- this.sessionId = Math.floor(Date.now() / 1000);
175
- this.started = true;
191
+ if (this.queueTimer !== null || this.eventQueue.length === 0) return;
176
192
  this.queueTimer = setInterval(
177
193
  () => this._processQueue().catch(console.error),
178
194
  this.queueInterval,
@@ -180,23 +196,39 @@ class AnalyticsApi implements IAnalyticsApi {
180
196
  }
181
197
 
182
198
  private _stopQueueProcessing(): void {
183
- if (!this.started) return;
184
- this.started = false;
185
- if (this.queueTimer) {
199
+ if (this.queueTimer !== null) {
186
200
  clearInterval(this.queueTimer);
201
+ this.queueTimer = null;
187
202
  }
188
203
  }
189
204
 
190
205
  private async _processQueue(): Promise<void> {
191
- if (this.eventQueue.length === 0) return;
192
- const events = this.eventQueue.splice(0, 5);
193
- await this._sendEvents(events);
194
- if (this.eventQueue.length === 0) {
195
- this._stopQueueProcessing();
206
+ if (this.processingQueue || this.eventQueue.length === 0) return;
207
+ this.processingQueue = true;
208
+ try {
209
+ const events = this.eventQueue.splice(0, 5);
210
+ await this._sendEvents(events);
211
+ } finally {
212
+ this.processingQueue = false;
213
+ if (this.eventQueue.length === 0) {
214
+ this._stopQueueProcessing();
215
+ }
196
216
  }
197
217
  }
198
218
 
199
219
  async _getCid(): Promise<string> {
220
+ if (this.cid) {
221
+ return this.cid;
222
+ }
223
+ if (!this.cidPromise) {
224
+ this.cidPromise = this._loadOrCreateCid().finally(() => {
225
+ this.cidPromise = undefined;
226
+ });
227
+ }
228
+ return this.cidPromise;
229
+ }
230
+
231
+ private async _loadOrCreateCid(): Promise<string> {
200
232
  this.cid = await getItem('analytics:cid');
201
233
  if (this.cid) {
202
234
  return this.cid;
@@ -235,7 +267,7 @@ class AnalyticsApi implements IAnalyticsApi {
235
267
  en: event.name,
236
268
  cid,
237
269
  pscdl: 'noapi',
238
- sid: String(this.sessionId),
270
+ sid: String(event.sessionId),
239
271
  'ep.origin': 'firebase',
240
272
  _z: 'fetch',
241
273
  _p: '' + Date.now(),
@@ -254,7 +286,7 @@ class AnalyticsApi implements IAnalyticsApi {
254
286
  queryParams.append('ep.debug_mode', '1');
255
287
  }
256
288
 
257
- if (this.consent && !this.consent.ad_personalization) {
289
+ if (this.consent.ad_personalization === false) {
258
290
  queryParams.append('npa', '1');
259
291
  } else {
260
292
  queryParams.append('npa', '0');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-firebase/analytics",
3
- "version": "26.3.3",
3
+ "version": "27.0.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": "./dist/module/index.js",
@@ -30,6 +30,7 @@
30
30
  "android:codegen": "node ../../scripts/codegen-package.mjs analytics android",
31
31
  "ios:codegen": "node ../../scripts/codegen-package.mjs analytics ios"
32
32
  },
33
+ "homepage": "https://rnfirebase.io",
33
34
  "repository": {
34
35
  "type": "git",
35
36
  "url": "https://github.com/invertase/react-native-firebase",
@@ -43,7 +44,7 @@
43
44
  "analytics"
44
45
  ],
45
46
  "peerDependencies": {
46
- "@react-native-firebase/app": "26.3.3",
47
+ "@react-native-firebase/app": "27.0.0",
47
48
  "expo": ">=47.0.0"
48
49
  },
49
50
  "publishConfig": {
@@ -54,7 +55,7 @@
54
55
  "superstruct": "^2.0.2"
55
56
  },
56
57
  "devDependencies": {
57
- "@react-native-firebase/app": "26.3.3",
58
+ "@react-native-firebase/app": "27.0.0",
58
59
  "expo": "^55.0.18",
59
60
  "react-native-builder-bob": "^0.41.0",
60
61
  "typescript": "^6.0.3"
@@ -102,5 +103,5 @@
102
103
  "node_modules/",
103
104
  "dist/"
104
105
  ],
105
- "gitHead": "04b90008385bf69be74f3a333d0dfea43ba32e64"
106
+ "gitHead": "0c7e942bca361f1765abaf233bf447d795718eff"
106
107
  }