@grafana/faro-react-native 1.2.0 → 1.3.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 (218) hide show
  1. package/android/build.gradle +118 -36
  2. package/android/consumer-proguard-rules.pro +2 -0
  3. package/android/src/main/java/com/grafana/faro/reactnative/ANRTracker.kt +61 -9
  4. package/android/src/main/java/com/grafana/faro/reactnative/AnrThreadDumpParser.kt +70 -0
  5. package/android/src/main/java/com/grafana/faro/reactnative/ApplicationExitTraceReader.kt +142 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrCache.kt +163 -0
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrReporter.kt +163 -0
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +243 -50
  9. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +75 -0
  10. package/android/src/main/java/com/grafana/faro/reactnative/FaroNativeCrashTrace.kt +39 -0
  11. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +99 -8
  12. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativePackage.kt +1 -0
  13. package/android/src/main/java/com/grafana/faro/reactnative/FaroUncaughtExceptionHandler.kt +62 -0
  14. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneBacktraceFormatter.kt +117 -0
  15. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneModels.kt +35 -0
  16. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneWireParser.kt +235 -0
  17. package/android/src/main/proto/tombstone.proto +62 -0
  18. package/android/src/test/java/com/grafana/faro/reactnative/AnrStackFiltersTest.kt +28 -0
  19. package/android/src/test/java/com/grafana/faro/reactnative/AnrThreadDumpParserTest.kt +38 -0
  20. package/android/src/test/java/com/grafana/faro/reactnative/FaroAnrCacheTest.kt +81 -0
  21. package/android/src/test/java/com/grafana/faro/reactnative/TombstoneWireParserTest.kt +51 -0
  22. package/dist/cjs/config/getRNInstrumentations.js +3 -1
  23. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  24. package/dist/cjs/config/makeRNConfig.js +48 -9
  25. package/dist/cjs/config/makeRNConfig.js.map +1 -1
  26. package/dist/cjs/config/types.js.map +1 -1
  27. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  28. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  29. package/dist/cjs/index.js +3 -2
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/initialize.js +14 -8
  32. package/dist/cjs/initialize.js.map +1 -1
  33. package/dist/cjs/instrumentations/anr/anrStack.js +19 -0
  34. package/dist/cjs/instrumentations/anr/anrStack.js.map +1 -0
  35. package/dist/cjs/instrumentations/anr/instrumentation.js +374 -72
  36. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/anr/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/console/index.js +5 -1
  39. package/dist/cjs/instrumentations/console/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +110 -103
  41. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  42. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +41 -0
  43. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  44. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +87 -0
  45. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  46. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js +123 -0
  47. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  48. package/dist/cjs/instrumentations/crashReporting/android/index.js +12 -0
  49. package/dist/cjs/instrumentations/crashReporting/android/index.js.map +1 -0
  50. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js +11 -0
  51. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  52. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +181 -0
  53. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  54. package/dist/cjs/instrumentations/crashReporting/index.js +35 -2
  55. package/dist/cjs/instrumentations/crashReporting/index.js.map +1 -1
  56. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +78 -0
  57. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  58. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js +103 -0
  59. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  60. package/dist/cjs/instrumentations/crashReporting/ios/index.js +11 -0
  61. package/dist/cjs/instrumentations/crashReporting/ios/index.js.map +1 -0
  62. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js +48 -0
  63. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  64. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  65. package/dist/cjs/instrumentations/errors/index.js +2 -1
  66. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  67. package/dist/cjs/instrumentations/session/installationId.js +91 -0
  68. package/dist/cjs/instrumentations/session/installationId.js.map +1 -0
  69. package/dist/cjs/instrumentations/session/sessionAttributes.js +152 -89
  70. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  71. package/dist/cjs/metas/appBuildIdentity.js +95 -0
  72. package/dist/cjs/metas/appBuildIdentity.js.map +1 -0
  73. package/dist/cjs/testUtils/mockDeviceInfo.js +5 -0
  74. package/dist/cjs/testUtils/mockDeviceInfo.js.map +1 -1
  75. package/dist/cjs/testUtils/setupTests.js +7 -0
  76. package/dist/cjs/testUtils/setupTests.js.map +1 -1
  77. package/dist/esm/config/getRNInstrumentations.js +3 -1
  78. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  79. package/dist/esm/config/makeRNConfig.js +49 -10
  80. package/dist/esm/config/makeRNConfig.js.map +1 -1
  81. package/dist/esm/config/types.js.map +1 -1
  82. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  83. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/initialize.js +11 -5
  87. package/dist/esm/initialize.js.map +1 -1
  88. package/dist/esm/instrumentations/anr/anrStack.js +16 -0
  89. package/dist/esm/instrumentations/anr/anrStack.js.map +1 -0
  90. package/dist/esm/instrumentations/anr/instrumentation.js +260 -67
  91. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  92. package/dist/esm/instrumentations/anr/types.js.map +1 -1
  93. package/dist/esm/instrumentations/console/index.js +5 -1
  94. package/dist/esm/instrumentations/console/index.js.map +1 -1
  95. package/dist/esm/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +76 -94
  96. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  97. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +18 -0
  98. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  99. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +56 -0
  100. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  101. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js +116 -0
  102. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  103. package/dist/esm/instrumentations/crashReporting/android/index.js +4 -0
  104. package/dist/esm/instrumentations/crashReporting/android/index.js.map +1 -0
  105. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js +8 -0
  106. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  107. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +174 -0
  108. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  109. package/dist/esm/instrumentations/crashReporting/index.js +35 -1
  110. package/dist/esm/instrumentations/crashReporting/index.js.map +1 -1
  111. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +56 -0
  112. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  113. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js +98 -0
  114. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  115. package/dist/esm/instrumentations/crashReporting/ios/index.js +4 -0
  116. package/dist/esm/instrumentations/crashReporting/ios/index.js.map +1 -0
  117. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js +44 -0
  118. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  119. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  120. package/dist/esm/instrumentations/errors/index.js +2 -1
  121. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  122. package/dist/esm/instrumentations/session/installationId.js +48 -0
  123. package/dist/esm/instrumentations/session/installationId.js.map +1 -0
  124. package/dist/esm/instrumentations/session/sessionAttributes.js +83 -38
  125. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  126. package/dist/esm/metas/appBuildIdentity.js +56 -0
  127. package/dist/esm/metas/appBuildIdentity.js.map +1 -0
  128. package/dist/esm/testUtils/mockDeviceInfo.js +5 -0
  129. package/dist/esm/testUtils/mockDeviceInfo.js.map +1 -1
  130. package/dist/esm/testUtils/setupTests.js +7 -0
  131. package/dist/esm/testUtils/setupTests.js.map +1 -1
  132. package/dist/types/config/makeRNConfig.d.ts +4 -3
  133. package/dist/types/config/types.d.ts +2 -1
  134. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  135. package/dist/types/index.d.ts +2 -2
  136. package/dist/types/initialize.d.ts +4 -2
  137. package/dist/types/instrumentations/anr/anrStack.d.ts +7 -0
  138. package/dist/types/instrumentations/anr/instrumentation.d.ts +23 -28
  139. package/dist/types/instrumentations/anr/types.d.ts +5 -1
  140. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  141. package/dist/types/instrumentations/console/index.d.ts +1 -1
  142. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +56 -0
  143. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +12 -0
  144. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +26 -0
  145. package/dist/types/instrumentations/crashReporting/android/crashErrorMessage.d.ts +23 -0
  146. package/dist/types/instrumentations/crashReporting/android/index.d.ts +4 -0
  147. package/dist/types/instrumentations/crashReporting/android/nativeTombstoneTrace.d.ts +2 -0
  148. package/dist/types/instrumentations/crashReporting/android/parseAndroidCrashTrace.d.ts +45 -0
  149. package/dist/types/instrumentations/crashReporting/index.d.ts +25 -2
  150. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +28 -0
  151. package/dist/types/instrumentations/crashReporting/ios/crashErrorMessage.d.ts +14 -0
  152. package/dist/types/instrumentations/crashReporting/ios/index.d.ts +4 -0
  153. package/dist/types/instrumentations/crashReporting/ios/parseIosCrashTrace.d.ts +38 -0
  154. package/dist/types/instrumentations/crashReporting/types.d.ts +18 -51
  155. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +1 -1
  156. package/dist/types/instrumentations/http/index.d.ts +1 -1
  157. package/dist/types/instrumentations/performance/index.d.ts +1 -1
  158. package/dist/types/instrumentations/session/index.d.ts +1 -1
  159. package/dist/types/instrumentations/session/installationId.d.ts +1 -0
  160. package/dist/types/instrumentations/session/sessionAttributes.d.ts +13 -4
  161. package/dist/types/instrumentations/startup/index.d.ts +1 -1
  162. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  163. package/dist/types/instrumentations/view/index.d.ts +1 -1
  164. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  165. package/dist/types/metas/appBuildIdentity.d.ts +16 -0
  166. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  167. package/dist/types/transports/console/transport.d.ts +1 -1
  168. package/dist/types/transports/fetch/transport.d.ts +1 -1
  169. package/dist/types/transports/offline/transport.d.ts +1 -1
  170. package/ios/faro-upload-composed-source-map.sh +8 -2
  171. package/package.json +5 -5
  172. package/src/config/getRNInstrumentations.ts +5 -1
  173. package/src/config/makeRNConfig.test.ts +59 -3
  174. package/src/config/makeRNConfig.ts +48 -9
  175. package/src/config/types.ts +2 -1
  176. package/src/generated/faroRNPackageMeta.ts +1 -1
  177. package/src/index.ts +2 -1
  178. package/src/initialize.test.ts +71 -15
  179. package/src/initialize.ts +11 -5
  180. package/src/instrumentations/anr/anrStack.test.ts +27 -0
  181. package/src/instrumentations/anr/anrStack.ts +19 -0
  182. package/src/instrumentations/anr/instrumentation.test.ts +45 -0
  183. package/src/instrumentations/anr/instrumentation.ts +301 -67
  184. package/src/instrumentations/anr/types.ts +7 -1
  185. package/src/instrumentations/console/index.ts +5 -1
  186. package/src/instrumentations/console/instrumentation.test.ts +5 -0
  187. package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
  188. package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
  189. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
  190. package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
  191. package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
  192. package/src/instrumentations/crashReporting/android/index.ts +4 -0
  193. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
  194. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
  195. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
  196. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
  197. package/src/instrumentations/crashReporting/index.ts +43 -2
  198. package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
  199. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
  200. package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
  201. package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
  202. package/src/instrumentations/crashReporting/ios/index.ts +4 -0
  203. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
  204. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
  205. package/src/instrumentations/crashReporting/types.ts +19 -58
  206. package/src/instrumentations/errors/index.ts +2 -1
  207. package/src/instrumentations/errors/instrumentation.test.ts +8 -2
  208. package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
  209. package/src/instrumentations/session/installationId.ts +43 -0
  210. package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
  211. package/src/instrumentations/session/sessionAttributes.ts +112 -22
  212. package/src/metas/appBuildIdentity.test.ts +48 -0
  213. package/src/metas/appBuildIdentity.ts +50 -0
  214. package/src/testUtils/mockDeviceInfo.ts +5 -0
  215. package/src/testUtils/setupTests.ts +7 -0
  216. package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
  217. package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
  218. package/dist/types/instrumentations/crashReporting/instrumentation.d.ts +0 -58
@@ -78,6 +78,7 @@ describe('ErrorsInstrumentation', () => {
78
78
  expect(item.payload.type).toBe('Error');
79
79
  expect(item.payload.value).toBe('Test error');
80
80
  expect(item.payload.context?.mechanism).toBe('uncaught');
81
+ expect(item.payload.fatal).toBe(false);
81
82
  });
82
83
 
83
84
  it('should include mechanism in context and use error.name for type (TypeError)', () => {
@@ -104,7 +105,7 @@ describe('ErrorsInstrumentation', () => {
104
105
  expect(item.payload.context?.mechanism).toBe('uncaught');
105
106
  });
106
107
 
107
- it('should include isFatal context', () => {
108
+ it('should use the React Native fatal signal for global JavaScript errors', () => {
108
109
  const transport = new MockTransport();
109
110
  let errorHandler: any;
110
111
 
@@ -124,7 +125,8 @@ describe('ErrorsInstrumentation', () => {
124
125
 
125
126
  expect(transport.items).toHaveLength(1);
126
127
  const item = transport.items[0] as TransportItem<ExceptionEvent>;
127
- expect(item.payload.context?.isFatal).toBe('true');
128
+ expect(item.payload.fatal).toBe(true);
129
+ expect(item.payload.context?.isFatal).toBeUndefined();
128
130
  });
129
131
 
130
132
  it('should call original error handler', () => {
@@ -399,6 +401,7 @@ describe('ErrorsInstrumentation', () => {
399
401
  expect(item.payload.value).toContain('Unhandled Promise Rejection');
400
402
  expect(item.payload.value).toContain('String rejection');
401
403
  expect(item.payload.context?.mechanism).toBe('unhandledrejection');
404
+ expect(item.payload.fatal).toBe(false);
402
405
  });
403
406
 
404
407
  it('should use actual error type for Error rejections with mechanism', () => {
@@ -527,6 +530,7 @@ describe('ErrorsInstrumentation', () => {
527
530
  describe('error handling resilience', () => {
528
531
  it('should not crash if error reporting fails', () => {
529
532
  let errorHandler: any;
533
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
530
534
 
531
535
  mockGlobal.ErrorUtils.setGlobalHandler = jest.fn((handler) => {
532
536
  errorHandler = handler;
@@ -534,6 +538,7 @@ describe('ErrorsInstrumentation', () => {
534
538
 
535
539
  // Create a transport that throws
536
540
  const failingTransport = {
541
+ isBatched: () => false,
537
542
  send: jest.fn(() => {
538
543
  throw new Error('Transport failed');
539
544
  }),
@@ -548,6 +553,7 @@ describe('ErrorsInstrumentation', () => {
548
553
 
549
554
  const testError = new Error('Test error');
550
555
  expect(() => errorHandler(testError, false)).not.toThrow();
556
+ consoleErrorSpy.mockRestore();
551
557
  });
552
558
 
553
559
  it('should handle errors without stack', () => {
@@ -205,7 +205,7 @@ describe('Stack Trace Parser', () => {
205
205
 
206
206
  it('collapses Hermes absolute .app bundle path to basename so uploads match ingest keys', () => {
207
207
  const iosPath =
208
- '/Users/ci/Library/Developer/CoreSimulator/Devices/ABC/Data/Containers/Bundle/Application/62A2/QuickPizza.app/main.jsbundle';
208
+ '/Users/ci/Library/Developer/CoreSimulator/Devices/ABC/Data/Containers/Bundle/Application/62A2/MyApp.app/main.jsbundle';
209
209
  const parsed = parseStackTraceLine(` at reportHandledException (${iosPath}:1:1249030)`);
210
210
  expect(parsed).not.toBeNull();
211
211
  const faroFrames = toFaroStackFrames([parsed!], { releaseBundleFilename: 'main.jsbundle' });
@@ -0,0 +1,43 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
+
3
+ const INSTALLATION_ID_STORAGE_KEY = '@grafana/faro-react-native/installation_id';
4
+
5
+ function createRandomBytes(length: number): Uint8Array {
6
+ const bytes = new Uint8Array(length);
7
+ const cryptoObject = globalThis.crypto;
8
+
9
+ if (cryptoObject && typeof cryptoObject.getRandomValues === 'function') {
10
+ cryptoObject.getRandomValues(bytes);
11
+ return bytes;
12
+ }
13
+
14
+ for (let i = 0; i < bytes.length; i++) {
15
+ bytes[i] = Math.floor(Math.random() * 256);
16
+ }
17
+
18
+ return bytes;
19
+ }
20
+
21
+ function generateInstallationId(): string {
22
+ const bytes = createRandomBytes(16);
23
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
24
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
25
+
26
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
27
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
28
+ }
29
+
30
+ export async function getInstallationId(): Promise<string | undefined> {
31
+ try {
32
+ const storedInstallationId = await AsyncStorage.getItem(INSTALLATION_ID_STORAGE_KEY);
33
+ if (storedInstallationId) {
34
+ return storedInstallationId;
35
+ }
36
+
37
+ const installationId = generateInstallationId();
38
+ await AsyncStorage.setItem(INSTALLATION_ID_STORAGE_KEY, installationId);
39
+ return installationId;
40
+ } catch (_error) {
41
+ return undefined;
42
+ }
43
+ }
@@ -1,7 +1,17 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage';
1
2
  import { Platform } from 'react-native';
2
3
  import DeviceInfo from 'react-native-device-info';
3
4
 
4
- import { getSessionAttributes } from './sessionAttributes';
5
+ import { getSessionAttributes, loadMobileMetaForInit } from './sessionAttributes';
6
+
7
+ const INSTALLATION_ID_STORAGE_KEY = '@grafana/faro-react-native/installation_id';
8
+ const STORED_INSTALLATION_ID = 'stored-installation-id';
9
+
10
+ function setStoredInstallationId(value = STORED_INSTALLATION_ID): void {
11
+ (global as any).mockAsyncStorage = {
12
+ [INSTALLATION_ID_STORAGE_KEY]: value,
13
+ };
14
+ }
5
15
 
6
16
  // Mock react-native-device-info
7
17
  jest.mock('react-native-device-info', () => ({
@@ -10,6 +20,7 @@ jest.mock('react-native-device-info', () => ({
10
20
  getSystemVersion: jest.fn(),
11
21
  getManufacturerSync: jest.fn(),
12
22
  getModel: jest.fn(),
23
+ getDeviceId: jest.fn(),
13
24
  getDeviceNameSync: jest.fn(),
14
25
  getBrand: jest.fn(),
15
26
  isEmulatorSync: jest.fn(),
@@ -20,10 +31,12 @@ jest.mock('react-native-device-info', () => ({
20
31
  isBatteryCharging: jest.fn(),
21
32
  getCarrier: jest.fn(),
22
33
  getApiLevel: jest.fn(),
34
+ getBuildId: jest.fn(),
23
35
  }));
24
36
 
25
37
  describe('sessionAttributes', () => {
26
38
  beforeEach(() => {
39
+ (global as any).mockAsyncStorage = {};
27
40
  jest.clearAllMocks();
28
41
  });
29
42
 
@@ -45,13 +58,14 @@ describe('sessionAttributes', () => {
45
58
  });
46
59
 
47
60
  it('should collect all iOS device attributes', async () => {
61
+ setStoredInstallationId();
48
62
  // Setup mocks for iOS device
49
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('ios-device-uuid-123');
50
63
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
51
64
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.0');
52
65
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
53
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone16,1');
54
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone 15 Pro');
66
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 15 Pro');
67
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone16,1');
68
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Vishwan's iPhone");
55
69
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
56
70
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
57
71
  (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
@@ -69,11 +83,11 @@ describe('sessionAttributes', () => {
69
83
  device_os_version: '17.0',
70
84
  device_os_detail: 'iOS 17.0',
71
85
  device_manufacturer: 'apple',
72
- device_model: 'iPhone16,1',
73
- device_model_name: 'iPhone 15 Pro',
86
+ device_model: 'iPhone 15 Pro',
87
+ device_model_name: "Vishwan's iPhone",
74
88
  device_brand: 'Apple',
75
89
  device_is_physical: 'true',
76
- device_id: 'ios-device-uuid-123',
90
+ device_id: STORED_INSTALLATION_ID,
77
91
  device_type: 'mobile',
78
92
  device_memory_total: '4000000000',
79
93
  device_memory_used: '2000000000',
@@ -85,12 +99,12 @@ describe('sessionAttributes', () => {
85
99
  });
86
100
 
87
101
  it('should identify emulator devices', async () => {
88
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('emulator-uuid');
89
102
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
90
103
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.0');
91
104
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
92
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('x86_64');
93
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone Simulator');
105
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone Simulator');
106
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('x86_64');
107
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('Test iPhone Simulator');
94
108
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
95
109
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(true);
96
110
  (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
@@ -100,16 +114,16 @@ describe('sessionAttributes', () => {
100
114
  const attributes = await getSessionAttributes();
101
115
 
102
116
  expect(attributes.device_is_physical).toBe('false');
103
- expect(attributes.device_model_name).toBe('iPhone Simulator');
117
+ expect(attributes.device_model_name).toBe('Test iPhone Simulator');
104
118
  });
105
119
 
106
120
  it('should handle iOS with different OS versions', async () => {
107
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('device-uuid');
108
121
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
109
122
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('16.4');
110
123
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
111
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone15,2');
112
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone 14 Pro');
124
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 14 Pro');
125
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone15,2');
126
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's iPhone");
113
127
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
114
128
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
115
129
  (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
@@ -140,12 +154,14 @@ describe('sessionAttributes', () => {
140
154
  });
141
155
 
142
156
  it('should collect all Android device attributes', async () => {
157
+ setStoredInstallationId();
143
158
  // Setup mocks for Android device
144
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('android-device-uuid-456');
145
159
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
146
160
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('15');
147
161
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Samsung');
148
162
  (DeviceInfo.getModel as jest.Mock).mockReturnValue('SM-A155F');
163
+ // Android getDeviceId() is the board code; structured model_identifier should use Build.MODEL instead.
164
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('a15');
149
165
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('SM-A155F');
150
166
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('samsung');
151
167
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
@@ -169,7 +185,7 @@ describe('sessionAttributes', () => {
169
185
  device_model_name: 'SM-A155F',
170
186
  device_brand: 'samsung',
171
187
  device_is_physical: 'true',
172
- device_id: 'android-device-uuid-456',
188
+ device_id: STORED_INSTALLATION_ID,
173
189
  device_type: 'mobile',
174
190
  device_memory_total: '8000000000',
175
191
  device_memory_used: '4000000000',
@@ -181,11 +197,11 @@ describe('sessionAttributes', () => {
181
197
  });
182
198
 
183
199
  it('should identify Android emulator devices', async () => {
184
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('emulator-uuid');
185
200
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
186
201
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('13');
187
202
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Google');
188
203
  (DeviceInfo.getModel as jest.Mock).mockReturnValue('sdk_gphone64_arm64');
204
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('emu64a');
189
205
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('Pixel 5');
190
206
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('google');
191
207
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(true);
@@ -201,11 +217,11 @@ describe('sessionAttributes', () => {
201
217
  });
202
218
 
203
219
  it('should handle Android without API level', async () => {
204
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('device-uuid');
205
220
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
206
221
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('12');
207
222
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Xiaomi');
208
223
  (DeviceInfo.getModel as jest.Mock).mockReturnValue('M2101K7AG');
224
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('camellia');
209
225
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('M2101K7AG');
210
226
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('xiaomi');
211
227
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
@@ -221,6 +237,218 @@ describe('sessionAttributes', () => {
221
237
  });
222
238
  });
223
239
 
240
+ describe('structured mobile meta', () => {
241
+ beforeEach(() => {
242
+ (Platform as any).OS = 'android';
243
+ Object.defineProperty(Platform, 'constants', {
244
+ value: {
245
+ reactNativeVersion: {
246
+ major: 0,
247
+ minor: 75,
248
+ patch: 1,
249
+ },
250
+ },
251
+ writable: true,
252
+ });
253
+ });
254
+
255
+ it('should collect structured app, device, and OS meta with flat session attributes', async () => {
256
+ setStoredInstallationId();
257
+ (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
258
+ (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('15');
259
+ (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Samsung');
260
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('SM-A155F');
261
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('a15');
262
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('Galaxy A15');
263
+ (DeviceInfo.getBrand as jest.Mock).mockReturnValue('samsung');
264
+ (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
265
+ (DeviceInfo.isTablet as jest.Mock).mockReturnValue(true);
266
+ (DeviceInfo.getTotalMemorySync as jest.Mock).mockReturnValue(8000000000);
267
+ (DeviceInfo.getUsedMemorySync as jest.Mock).mockReturnValue(4000000000);
268
+ (DeviceInfo.getApiLevel as jest.Mock).mockResolvedValue(35);
269
+ (DeviceInfo.getBuildId as jest.Mock).mockResolvedValue('AP3A.240905.015.A2');
270
+
271
+ const mobileMeta = await loadMobileMetaForInit();
272
+
273
+ expect(mobileMeta.meta).toEqual({
274
+ app: {
275
+ installationId: STORED_INSTALLATION_ID,
276
+ },
277
+ device: {
278
+ brand: 'samsung',
279
+ is_physical: true,
280
+ manufacturer: 'Samsung',
281
+ model_identifier: 'SM-A155F',
282
+ model_name: 'SM-A155F',
283
+ type: 'tablet',
284
+ },
285
+ os: {
286
+ build_id: 'AP3A.240905.015.A2',
287
+ detail: 'Android 15 (SDK 35)',
288
+ name: 'Android',
289
+ version: '15',
290
+ },
291
+ });
292
+ expect(mobileMeta.sessionAttributes).toMatchObject({
293
+ device_id: STORED_INSTALLATION_ID,
294
+ device_model: 'SM-A155F',
295
+ device_os: 'Android',
296
+ device_os_detail: 'Android 15 (SDK 35)',
297
+ device_type: 'tablet',
298
+ });
299
+ expect(AsyncStorage.getItem).toHaveBeenCalledWith(INSTALLATION_ID_STORAGE_KEY);
300
+ expect(AsyncStorage.setItem).not.toHaveBeenCalled();
301
+ });
302
+
303
+ it('should collect iOS structured app, device, and OS meta with flat session attributes', async () => {
304
+ (Platform as any).OS = 'ios';
305
+ setStoredInstallationId();
306
+ (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
307
+ (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.2');
308
+ (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
309
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 15 Pro');
310
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone16,1');
311
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Vishwan's iPhone");
312
+ (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
313
+ (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
314
+ (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
315
+ (DeviceInfo.getTotalMemorySync as jest.Mock).mockReturnValue(4000000000);
316
+ (DeviceInfo.getUsedMemorySync as jest.Mock).mockReturnValue(2000000000);
317
+ (DeviceInfo.getBuildId as jest.Mock).mockResolvedValue('21C62');
318
+
319
+ const mobileMeta = await loadMobileMetaForInit();
320
+
321
+ expect(mobileMeta.meta).toEqual({
322
+ app: {
323
+ installationId: STORED_INSTALLATION_ID,
324
+ },
325
+ device: {
326
+ brand: 'iPhone',
327
+ is_physical: true,
328
+ manufacturer: 'apple',
329
+ model_identifier: 'iPhone16,1',
330
+ model_name: 'iPhone 15 Pro',
331
+ type: 'mobile',
332
+ },
333
+ os: {
334
+ build_id: '21C62',
335
+ detail: 'iOS 17.2',
336
+ name: 'iOS',
337
+ version: '17.2',
338
+ },
339
+ });
340
+ expect(mobileMeta.sessionAttributes).toMatchObject({
341
+ device_id: STORED_INSTALLATION_ID,
342
+ device_model: 'iPhone 15 Pro',
343
+ device_os: 'iOS',
344
+ device_os_detail: 'iOS 17.2',
345
+ device_type: 'mobile',
346
+ });
347
+ expect(mobileMeta.meta.device?.model_name).not.toContain('Vishwan');
348
+ });
349
+
350
+ it('should classify iPad structured meta as iPad tablet', async () => {
351
+ (Platform as any).OS = 'ios';
352
+ setStoredInstallationId();
353
+ (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iPadOS');
354
+ (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('18.1');
355
+ (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
356
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPad Pro');
357
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPad14,3');
358
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's iPad");
359
+ (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
360
+ (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
361
+ (DeviceInfo.isTablet as jest.Mock).mockReturnValue(true);
362
+ (DeviceInfo.getTotalMemorySync as jest.Mock).mockReturnValue(8000000000);
363
+ (DeviceInfo.getUsedMemorySync as jest.Mock).mockReturnValue(3000000000);
364
+ (DeviceInfo.getBuildId as jest.Mock).mockResolvedValue('22B83');
365
+
366
+ const mobileMeta = await loadMobileMetaForInit();
367
+
368
+ expect(mobileMeta.meta.device).toMatchObject({
369
+ brand: 'iPad',
370
+ manufacturer: 'apple',
371
+ model_identifier: 'iPad14,3',
372
+ model_name: 'iPad Pro',
373
+ type: 'tablet',
374
+ });
375
+ expect(mobileMeta.meta.os).toMatchObject({
376
+ build_id: '22B83',
377
+ detail: 'iPadOS 18.1',
378
+ name: 'iPadOS',
379
+ version: '18.1',
380
+ });
381
+ });
382
+
383
+ it('should omit mobile/tablet type when running on a non-mobile React Native platform', async () => {
384
+ (Platform as any).OS = 'macos';
385
+ setStoredInstallationId();
386
+ (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('macOS');
387
+ (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('15.0');
388
+ (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
389
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('MacBookPro18,3');
390
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('MacBookPro18,3');
391
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's MacBook Pro");
392
+ (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
393
+ (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
394
+ (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
395
+ (DeviceInfo.getTotalMemorySync as jest.Mock).mockReturnValue(16000000000);
396
+ (DeviceInfo.getUsedMemorySync as jest.Mock).mockReturnValue(8000000000);
397
+ (DeviceInfo.getBuildId as jest.Mock).mockResolvedValue('24A335');
398
+
399
+ const mobileMeta = await loadMobileMetaForInit();
400
+
401
+ expect(mobileMeta.sessionAttributes.device_type).toBeUndefined();
402
+ expect(mobileMeta.meta.device).not.toHaveProperty('type');
403
+ });
404
+
405
+ it('should create and persist an SDK installation id when one does not exist yet', async () => {
406
+ (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
407
+ (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('15');
408
+ (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Google');
409
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('sdk_gphone64_arm64');
410
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('emu64a');
411
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('Pixel 5');
412
+ (DeviceInfo.getBrand as jest.Mock).mockReturnValue('google');
413
+ (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(true);
414
+ (DeviceInfo.isTablet as jest.Mock).mockReturnValue(false);
415
+ (DeviceInfo.getTotalMemorySync as jest.Mock).mockReturnValue(2000000000);
416
+ (DeviceInfo.getUsedMemorySync as jest.Mock).mockReturnValue(1000000000);
417
+ (DeviceInfo.getApiLevel as jest.Mock).mockResolvedValue(35);
418
+ (DeviceInfo.getBuildId as jest.Mock).mockResolvedValue('AP3A.240905.015.A2');
419
+
420
+ const mobileMeta = await loadMobileMetaForInit();
421
+
422
+ expect(mobileMeta.meta.app?.installationId).toMatch(
423
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
424
+ );
425
+ expect(AsyncStorage.setItem).toHaveBeenCalledWith(
426
+ INSTALLATION_ID_STORAGE_KEY,
427
+ mobileMeta.meta.app?.installationId
428
+ );
429
+ });
430
+
431
+ it('should keep installation id when DeviceInfo collection fails', async () => {
432
+ setStoredInstallationId();
433
+ (DeviceInfo.getSystemName as jest.Mock).mockImplementation(() => {
434
+ throw new Error('Failed to get system name');
435
+ });
436
+
437
+ const mobileMeta = await loadMobileMetaForInit();
438
+
439
+ expect(mobileMeta).toEqual({
440
+ sessionAttributes: {
441
+ react_native_version: '0.75.1',
442
+ },
443
+ meta: {
444
+ app: {
445
+ installationId: STORED_INSTALLATION_ID,
446
+ },
447
+ },
448
+ });
449
+ });
450
+ });
451
+
224
452
  describe('React Native version parsing', () => {
225
453
  it('should parse version with prerelease', () => {
226
454
  Object.defineProperty(Platform, 'constants', {
@@ -235,12 +463,12 @@ describe('sessionAttributes', () => {
235
463
  writable: true,
236
464
  });
237
465
 
238
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('uuid');
239
466
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
240
467
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.0');
241
468
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
242
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone16,1');
243
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone 15 Pro');
469
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 15 Pro');
470
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone16,1');
471
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's iPhone");
244
472
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
245
473
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
246
474
 
@@ -255,12 +483,12 @@ describe('sessionAttributes', () => {
255
483
  writable: true,
256
484
  });
257
485
 
258
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('uuid');
259
486
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
260
487
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.0');
261
488
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('Apple');
262
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone16,1');
263
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone 15 Pro');
489
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 15 Pro');
490
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone16,1');
491
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's iPhone");
264
492
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
265
493
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
266
494
 
@@ -273,7 +501,6 @@ describe('sessionAttributes', () => {
273
501
  describe('Error handling', () => {
274
502
  it('should gracefully handle DeviceInfo errors and return fallback values', async () => {
275
503
  // Mock all DeviceInfo methods to throw errors
276
- (DeviceInfo.getUniqueId as jest.Mock).mockRejectedValue(new Error('Failed to get UUID'));
277
504
  (DeviceInfo.getSystemName as jest.Mock).mockImplementation(() => {
278
505
  throw new Error('Failed to get system name');
279
506
  });
@@ -286,6 +513,9 @@ describe('sessionAttributes', () => {
286
513
  (DeviceInfo.getModel as jest.Mock).mockImplementation(() => {
287
514
  throw new Error('Failed to get model');
288
515
  });
516
+ (DeviceInfo.getDeviceId as jest.Mock).mockImplementation(() => {
517
+ throw new Error('Failed to get device id');
518
+ });
289
519
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockImplementation(() => {
290
520
  throw new Error('Failed to get device name');
291
521
  });
@@ -309,14 +539,14 @@ describe('sessionAttributes', () => {
309
539
  it('should handle partial DeviceInfo failures', async () => {
310
540
  // Some methods work, others fail
311
541
  // Note: If any method throws, the entire catch block returns fallback values
312
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('valid-uuid');
313
542
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('iOS');
314
543
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('17.0');
315
544
  (DeviceInfo.getManufacturerSync as jest.Mock).mockImplementation(() => {
316
545
  throw new Error('Manufacturer unavailable');
317
546
  });
318
- (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone16,1');
319
- (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('iPhone 15 Pro');
547
+ (DeviceInfo.getModel as jest.Mock).mockReturnValue('iPhone 15 Pro');
548
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('iPhone16,1');
549
+ (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue("Test's iPhone");
320
550
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('Apple');
321
551
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
322
552
 
@@ -334,11 +564,11 @@ describe('sessionAttributes', () => {
334
564
 
335
565
  describe('Manufacturer normalization', () => {
336
566
  it('should lowercase manufacturer names', async () => {
337
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('uuid');
338
567
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
339
568
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('14');
340
569
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('SAMSUNG');
341
570
  (DeviceInfo.getModel as jest.Mock).mockReturnValue('SM-G998B');
571
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('p3s');
342
572
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('Galaxy S21 Ultra');
343
573
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('samsung');
344
574
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);
@@ -353,11 +583,11 @@ describe('sessionAttributes', () => {
353
583
  });
354
584
 
355
585
  it('should handle mixed case manufacturer names', async () => {
356
- (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('uuid');
357
586
  (DeviceInfo.getSystemName as jest.Mock).mockReturnValue('Android');
358
587
  (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('14');
359
588
  (DeviceInfo.getManufacturerSync as jest.Mock).mockReturnValue('OnePlus');
360
589
  (DeviceInfo.getModel as jest.Mock).mockReturnValue('LE2121');
590
+ (DeviceInfo.getDeviceId as jest.Mock).mockReturnValue('lemonade');
361
591
  (DeviceInfo.getDeviceNameSync as jest.Mock).mockReturnValue('OnePlus 9 Pro');
362
592
  (DeviceInfo.getBrand as jest.Mock).mockReturnValue('OnePlus');
363
593
  (DeviceInfo.isEmulatorSync as jest.Mock).mockReturnValue(false);