@grafana/faro-react-native 1.3.1 → 1.4.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 (256) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +17 -12
  3. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
  4. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
  5. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
  9. package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
  10. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
  11. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
  12. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
  13. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
  14. package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
  15. package/dist/cjs/config/getRNInstrumentations.js +6 -8
  16. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  17. package/dist/cjs/config/sampling.js.map +1 -1
  18. package/dist/cjs/config/types.js.map +1 -1
  19. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  20. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  21. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  22. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  23. package/dist/cjs/index.js +8 -3
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  26. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  27. package/dist/cjs/instrumentations/appState/index.js +8 -2
  28. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  29. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  30. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  32. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  33. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  34. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  35. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/errors/index.js +8 -3
  39. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  41. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  43. package/dist/cjs/instrumentations/performance/index.js +2 -6
  44. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  45. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  46. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  47. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  48. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/index.js +214 -45
  50. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  51. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  52. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  53. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  54. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  55. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  56. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  58. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  60. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  61. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  62. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  66. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  67. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  68. package/dist/cjs/instrumentations/startup/index.js +2 -6
  69. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  70. package/dist/cjs/navigation/utils.js +3 -1
  71. package/dist/cjs/navigation/utils.js.map +1 -1
  72. package/dist/cjs/transports/fetch/index.js.map +1 -1
  73. package/dist/cjs/transports/fetch/transport.js +56 -25
  74. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  75. package/dist/cjs/transports/fetch/types.js.map +1 -1
  76. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  77. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  78. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  79. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  80. package/dist/cjs/transports/offline/transport.js +3 -4
  81. package/dist/cjs/transports/offline/transport.js.map +1 -1
  82. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  83. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  84. package/dist/esm/config/getRNInstrumentations.js +6 -8
  85. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  86. package/dist/esm/config/sampling.js.map +1 -1
  87. package/dist/esm/config/types.js.map +1 -1
  88. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  89. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  90. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  91. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  92. package/dist/esm/index.js +4 -2
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  95. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  96. package/dist/esm/instrumentations/appState/index.js +8 -2
  97. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  98. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  99. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  101. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  102. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  103. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  105. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  106. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  107. package/dist/esm/instrumentations/errors/index.js +8 -3
  108. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  109. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  110. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  111. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  112. package/dist/esm/instrumentations/performance/index.js +2 -6
  113. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  114. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  115. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  116. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  117. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  118. package/dist/esm/instrumentations/session/index.js +207 -46
  119. package/dist/esm/instrumentations/session/index.js.map +1 -1
  120. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  121. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  123. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  125. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  126. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  127. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  129. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  130. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  131. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  133. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  134. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  135. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  136. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  137. package/dist/esm/instrumentations/startup/index.js +2 -6
  138. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  139. package/dist/esm/navigation/utils.js +2 -1
  140. package/dist/esm/navigation/utils.js.map +1 -1
  141. package/dist/esm/transports/fetch/index.js.map +1 -1
  142. package/dist/esm/transports/fetch/transport.js +38 -14
  143. package/dist/esm/transports/fetch/transport.js.map +1 -1
  144. package/dist/esm/transports/fetch/types.js.map +1 -1
  145. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  146. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  147. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  148. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  149. package/dist/esm/transports/offline/transport.js +3 -4
  150. package/dist/esm/transports/offline/transport.js.map +1 -1
  151. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  152. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  153. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  154. package/dist/types/config/sampling.d.ts +0 -3
  155. package/dist/types/config/types.d.ts +5 -3
  156. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  157. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  158. package/dist/types/index.d.ts +4 -1
  159. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  160. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  161. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
  162. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
  163. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  164. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
  165. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  166. package/dist/types/instrumentations/performance/index.d.ts +0 -2
  167. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  168. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  169. package/dist/types/instrumentations/session/index.d.ts +20 -1
  170. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  171. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  172. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  173. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  174. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  175. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  176. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  177. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  178. package/dist/types/instrumentations/startup/index.d.ts +2 -6
  179. package/dist/types/navigation/utils.d.ts +1 -0
  180. package/dist/types/transports/fetch/index.d.ts +1 -1
  181. package/dist/types/transports/fetch/transport.d.ts +8 -1
  182. package/dist/types/transports/fetch/types.d.ts +5 -0
  183. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  184. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  185. package/dist/types/transports/offline/transport.d.ts +3 -4
  186. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  187. package/ios/FaroCrashReporter.swift +214 -21
  188. package/ios/FaroReactNative.swift +24 -0
  189. package/ios/FaroReactNativeModule.h +2 -1
  190. package/ios/FaroReactNativeModule.mm +121 -6
  191. package/ios/RefreshRateVitals.swift +34 -38
  192. package/package.json +7 -5
  193. package/src/config/getRNInstrumentations.ts +6 -8
  194. package/src/config/makeRNConfig.test.ts +6 -0
  195. package/src/config/sampling.ts +0 -3
  196. package/src/config/types.ts +5 -3
  197. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  198. package/src/generated/faroRNPackageMeta.ts +1 -1
  199. package/src/index.ts +8 -2
  200. package/src/instrumentations/anr/instrumentation.ts +8 -1
  201. package/src/instrumentations/appState/index.ts +10 -2
  202. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  203. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  204. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  205. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  206. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  207. package/src/instrumentations/crashReporting/types.ts +7 -0
  208. package/src/instrumentations/errors/index.ts +12 -4
  209. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  210. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  211. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  212. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  213. package/src/instrumentations/performance/index.ts +2 -6
  214. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  215. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  216. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  217. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  218. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  219. package/src/instrumentations/session/index.ts +269 -46
  220. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  221. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  222. package/src/instrumentations/session/sessionActivity.ts +58 -0
  223. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  224. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  225. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  226. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  227. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  228. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  229. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  230. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  231. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  232. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  233. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  234. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  235. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  236. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  237. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  238. package/src/instrumentations/session/sessionProcess.ts +114 -0
  239. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  240. package/src/instrumentations/startup/index.ts +2 -6
  241. package/src/navigation/utils.ts +3 -1
  242. package/src/transports/fetch/index.ts +6 -1
  243. package/src/transports/fetch/transport.test.ts +86 -2
  244. package/src/transports/fetch/transport.ts +38 -16
  245. package/src/transports/fetch/types.ts +7 -0
  246. package/src/transports/offline/ConnectivityService.ts +1 -1
  247. package/src/transports/offline/OfflineCache.ts +2 -2
  248. package/src/transports/offline/transport.ts +3 -4
  249. package/src/userPersistence/UserPersistence.ts +1 -2
  250. package/dist/cjs/utils/throttle.js +0 -42
  251. package/dist/cjs/utils/throttle.js.map +0 -1
  252. package/dist/esm/utils/throttle.js +0 -34
  253. package/dist/esm/utils/throttle.js.map +0 -1
  254. package/dist/types/utils/throttle.d.ts +0 -9
  255. package/src/utils/throttle.test.ts +0 -141
  256. package/src/utils/throttle.ts +0 -34
@@ -1,7 +1,10 @@
1
1
  import * as faroCore from '@grafana/faro-core';
2
- import { initializeFaro } from '@grafana/faro-core';
2
+ import { initializeFaro, stringifyExternalJson } from '@grafana/faro-core';
3
+ import type { Faro, MetaAttributes } from '@grafana/faro-core';
3
4
  import { mockConfig } from '@grafana/faro-test-utils';
4
5
 
6
+ import { SessionActivityKind } from '../sessionActivity';
7
+
5
8
  import * as samplingModule from './sampling';
6
9
  import {
7
10
  addSessionMetadataToNextSession,
@@ -140,6 +143,13 @@ describe('sessionManagerUtils', () => {
140
143
  const isValid = isUserSessionValid(session);
141
144
  expect(isValid).toBe(true);
142
145
  });
146
+
147
+ it.each(['started', 'lastActivity'] as const)('returns false when %s is in the future', (timestamp) => {
148
+ const session = createUserSessionObject();
149
+ session[timestamp] = fakeSystemTime + 1;
150
+
151
+ expect(isUserSessionValid(session)).toBe(false);
152
+ });
143
153
  });
144
154
 
145
155
  describe('addSessionMetadataToNextSession', () => {
@@ -284,6 +294,24 @@ describe('sessionManagerUtils', () => {
284
294
  });
285
295
 
286
296
  describe('getUserSessionUpdater', () => {
297
+ it('uses an already fetched session without reading storage again', () => {
298
+ initializeFaro(mockConfig({}));
299
+ const existingSession: FaroUserSession = {
300
+ sessionId: mockSessionId,
301
+ started: fakeSystemTime,
302
+ lastActivity: fakeSystemTime - 1000,
303
+ isSampled: true,
304
+ };
305
+ const mockFetchUserSession = jest.fn();
306
+ const updateSession = getUserSessionUpdater({
307
+ fetchUserSession: mockFetchUserSession,
308
+ storeUserSession: jest.fn(),
309
+ });
310
+
311
+ expect(updateSession(SessionActivityKind.Passive, existingSession)).toBe(existingSession);
312
+ expect(mockFetchUserSession).not.toHaveBeenCalled();
313
+ });
314
+
287
315
  it('updates session when session is invalid', async () => {
288
316
  const mockOnSessionChange = jest.fn();
289
317
  const config = mockConfig({
@@ -295,8 +323,8 @@ describe('sessionManagerUtils', () => {
295
323
 
296
324
  const _faro = initializeFaro(config);
297
325
 
298
- const mockFetchUserSession = jest.fn().mockResolvedValue(null);
299
- const mockStoreUserSession = jest.fn().mockResolvedValue(undefined);
326
+ const mockFetchUserSession = jest.fn().mockReturnValue(null);
327
+ const mockStoreUserSession = jest.fn();
300
328
 
301
329
  const updateSession = getUserSessionUpdater({
302
330
  fetchUserSession: mockFetchUserSession,
@@ -306,7 +334,7 @@ describe('sessionManagerUtils', () => {
306
334
  jest.spyOn(faroCore, 'genShortID').mockReturnValueOnce(mockSessionId);
307
335
  jest.spyOn(samplingModule, 'isSampled').mockReturnValueOnce(true);
308
336
 
309
- await updateSession();
337
+ updateSession(SessionActivityKind.Passive);
310
338
 
311
339
  expect(mockFetchUserSession).toHaveBeenCalledTimes(1);
312
340
  expect(mockStoreUserSession).toHaveBeenCalledTimes(1);
@@ -329,29 +357,52 @@ describe('sessionManagerUtils', () => {
329
357
  isSampled: true,
330
358
  };
331
359
 
332
- const mockFetchUserSession = jest.fn().mockResolvedValue(existingSession);
333
- const mockStoreUserSession = jest.fn().mockResolvedValue(undefined);
360
+ const mockFetchUserSession = jest.fn().mockReturnValue(existingSession);
361
+ const mockStoreUserSession = jest.fn();
334
362
 
335
363
  const updateSession = getUserSessionUpdater({
336
364
  fetchUserSession: mockFetchUserSession,
337
365
  storeUserSession: mockStoreUserSession,
338
366
  });
339
367
 
340
- await updateSession();
368
+ updateSession(SessionActivityKind.Meaningful);
341
369
 
342
370
  expect(mockStoreUserSession).toHaveBeenCalledWith({
343
371
  ...existingSession,
344
372
  lastActivity: fakeSystemTime,
345
373
  });
346
374
  });
375
+
376
+ it('keeps the new session when onSessionChange throws', () => {
377
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
378
+ initializeFaro(
379
+ mockConfig({
380
+ sessionTracking: {
381
+ enabled: true,
382
+ onSessionChange: () => {
383
+ throw new Error('callback failed');
384
+ },
385
+ },
386
+ })
387
+ );
388
+ const mockStoreUserSession = jest.fn();
389
+ const updateSession = getUserSessionUpdater({
390
+ fetchUserSession: jest.fn().mockReturnValue(null),
391
+ storeUserSession: mockStoreUserSession,
392
+ });
393
+
394
+ expect(() => updateSession(SessionActivityKind.Passive)).not.toThrow();
395
+ expect(mockStoreUserSession).toHaveBeenCalledTimes(1);
396
+ expect(warnSpy).toHaveBeenCalledWith('The session change callback failed:', expect.any(Error));
397
+ });
347
398
  });
348
399
 
349
400
  describe('getSessionMetaUpdateHandler', () => {
350
401
  it('creates new session when session ID changes', async () => {
351
402
  initializeFaro(mockConfig({}));
352
403
 
353
- const mockFetchUserSession = jest.fn().mockResolvedValue(null);
354
- const mockStoreUserSession = jest.fn().mockResolvedValue(undefined);
404
+ const mockFetchUserSession = jest.fn().mockReturnValue(null);
405
+ const mockStoreUserSession = jest.fn();
355
406
 
356
407
  const handler = getSessionMetaUpdateHandler({
357
408
  fetchUserSession: mockFetchUserSession,
@@ -376,23 +427,36 @@ describe('sessionManagerUtils', () => {
376
427
  });
377
428
 
378
429
  it('updates attributes without creating new session', async () => {
379
- const faro = initializeFaro(mockConfig({}));
430
+ const faro = initializeFaro(
431
+ mockConfig({
432
+ sessionTracking: {
433
+ session: {
434
+ attributes: {
435
+ configured: 'value',
436
+ },
437
+ },
438
+ },
439
+ })
440
+ );
441
+ const samplingSpy = jest.spyOn(samplingModule, 'isSampled');
380
442
 
381
443
  const storedSession: FaroUserSession = {
382
444
  sessionId: mockSessionId,
383
- isSampled: true,
384
- lastActivity: fakeSystemTime,
385
- started: fakeSystemTime,
445
+ isSampled: false,
446
+ lastActivity: fakeSystemTime - 500,
447
+ started: fakeSystemTime - 1000,
386
448
  sessionMeta: {
387
449
  id: mockSessionId,
388
450
  attributes: {
389
- isSampled: 'true',
451
+ isSampled: 'false',
452
+ previousSession: 'previous-session',
390
453
  },
454
+ overrides: { serviceName: 'stored-service' },
391
455
  },
392
456
  };
393
457
 
394
- const mockFetchUserSession = jest.fn().mockResolvedValue(storedSession);
395
- const mockStoreUserSession = jest.fn().mockResolvedValue(undefined);
458
+ const mockFetchUserSession = jest.fn().mockReturnValue(storedSession);
459
+ const mockStoreUserSession = jest.fn();
396
460
 
397
461
  const handler = getSessionMetaUpdateHandler({
398
462
  fetchUserSession: mockFetchUserSession,
@@ -412,13 +476,65 @@ describe('sessionManagerUtils', () => {
412
476
  expect(mockStoreUserSession).toHaveBeenCalledWith(
413
477
  expect.objectContaining({
414
478
  sessionId: mockSessionId,
479
+ isSampled: false,
480
+ started: fakeSystemTime - 1000,
481
+ lastActivity: fakeSystemTime - 500,
415
482
  sessionMeta: expect.objectContaining({
416
483
  attributes: expect.objectContaining({
417
484
  foo: 'bar',
485
+ isSampled: 'false',
486
+ previousSession: 'previous-session',
487
+ configured: 'value',
418
488
  }),
489
+ overrides: { serviceName: 'stored-service' },
419
490
  }),
420
491
  })
421
492
  );
493
+ expect(samplingSpy).not.toHaveBeenCalled();
494
+ });
495
+
496
+ it('restores internal attributes when the same session is set without metadata', async () => {
497
+ const faro = initializeFaro(mockConfig({}));
498
+ const storedSession: FaroUserSession = {
499
+ sessionId: mockSessionId,
500
+ isSampled: false,
501
+ lastActivity: fakeSystemTime - 500,
502
+ started: fakeSystemTime - 1000,
503
+ sessionMeta: {
504
+ id: mockSessionId,
505
+ attributes: {
506
+ isSampled: 'false',
507
+ previousSession: 'previous-session',
508
+ },
509
+ },
510
+ };
511
+ const mockStoreUserSession = jest.fn();
512
+ const handler = getSessionMetaUpdateHandler({
513
+ fetchUserSession: jest.fn().mockReturnValue(storedSession),
514
+ storeUserSession: mockStoreUserSession,
515
+ });
516
+
517
+ await handler({ session: { id: mockSessionId } });
518
+
519
+ expect(mockStoreUserSession).toHaveBeenCalledWith(
520
+ expect.objectContaining({
521
+ isSampled: false,
522
+ sessionMeta: {
523
+ id: mockSessionId,
524
+ attributes: {
525
+ isSampled: 'false',
526
+ previousSession: 'previous-session',
527
+ },
528
+ },
529
+ })
530
+ );
531
+ expect(faro.api.getSession()).toStrictEqual({
532
+ id: mockSessionId,
533
+ attributes: {
534
+ isSampled: 'false',
535
+ previousSession: 'previous-session',
536
+ },
537
+ });
422
538
  });
423
539
 
424
540
  it('sends service name override event when service name changes', async () => {
@@ -431,8 +547,8 @@ describe('sessionManagerUtils', () => {
431
547
  })
432
548
  );
433
549
 
434
- const mockFetchUserSession = jest.fn().mockResolvedValue(null);
435
- const mockStoreUserSession = jest.fn().mockResolvedValue(undefined);
550
+ const mockFetchUserSession = jest.fn().mockReturnValue(null);
551
+ const mockStoreUserSession = jest.fn();
436
552
 
437
553
  const handler = getSessionMetaUpdateHandler({
438
554
  fetchUserSession: mockFetchUserSession,
@@ -456,5 +572,127 @@ describe('sessionManagerUtils', () => {
456
572
  previousServiceName: 'my-app',
457
573
  });
458
574
  });
575
+
576
+ // Round-trips sessions through the same lossy serialization persistent
577
+ // storage uses: `stringifyExternalJson` drops keys whose value is
578
+ // `undefined`.
579
+ function createJsonBackedSessionStorage() {
580
+ let stored: string | null = null;
581
+
582
+ return {
583
+ storeUserSession: jest.fn((session: FaroUserSession) => {
584
+ stored = stringifyExternalJson(session);
585
+ }),
586
+ fetchUserSession: jest.fn((): FaroUserSession | null => (stored == null ? null : JSON.parse(stored))),
587
+ };
588
+ }
589
+
590
+ describe('settling', () => {
591
+ let faro: Faro;
592
+ let storage: ReturnType<typeof createJsonBackedSessionStorage>;
593
+ let handlers: Array<ReturnType<typeof getSessionMetaUpdateHandler>>;
594
+
595
+ function attachHandler(): void {
596
+ const handler = getSessionMetaUpdateHandler(storage);
597
+ faro.metas.addListener(handler);
598
+ handlers.push(handler);
599
+ }
600
+
601
+ beforeEach(() => {
602
+ faro = initializeFaro(mockConfig({}));
603
+ jest.spyOn(samplingModule, 'isSampled').mockReturnValue(true);
604
+ storage = createJsonBackedSessionStorage();
605
+ handlers = [];
606
+
607
+ // Stop propagating session updates past a sane number of calls, so a
608
+ // regression that reintroduces the sync loop fails on the write-count
609
+ // assertions below instead of exhausting the heap.
610
+ const setSession = faro.api.setSession.bind(faro.api);
611
+ const setSessionSpy = jest.spyOn(faro.api, 'setSession').mockImplementation((...args) => {
612
+ if (setSessionSpy.mock.calls.length <= 10) {
613
+ setSession(...args);
614
+ }
615
+ });
616
+ });
617
+
618
+ afterEach(() => {
619
+ handlers.forEach((handler) => faro.metas.removeListener(handler));
620
+ });
621
+
622
+ it('settles after applying its own update and still accepts later external changes', () => {
623
+ attachHandler();
624
+
625
+ // The handler applies the external change once; the `setSession()` it
626
+ // performs itself must not be picked up as another external change.
627
+ faro.api.setSession({ id: mockSessionId });
628
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(1);
629
+
630
+ // A genuinely external change afterwards is still synced, so the guard
631
+ // did not leave the handler permanently disabled.
632
+ faro.api.setSession({ id: 'next-session-id' });
633
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(2);
634
+ expect(storage.storeUserSession).toHaveBeenLastCalledWith(
635
+ expect.objectContaining({ sessionId: 'next-session-id' })
636
+ );
637
+ });
638
+
639
+ it('does not write the session again when an unrelated meta changes', () => {
640
+ attachHandler();
641
+
642
+ // An attribute whose value is `undefined` mirrors the optional device
643
+ // attributes (battery level, carrier, ...). Storage drops the key on
644
+ // write; the in-memory meta must converge to the same shape.
645
+ faro.api.setSession({
646
+ id: mockSessionId,
647
+ attributes: { device_battery_level: undefined } as unknown as MetaAttributes,
648
+ });
649
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(1);
650
+
651
+ // Unrelated meta updates notify the handler with an unchanged session;
652
+ // none of them may write the session again.
653
+ faro.api.setView({ name: 'first-view' });
654
+ faro.api.setUser({ id: 'user-1' });
655
+
656
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(1);
657
+ });
658
+
659
+ it('does not rewrite the session when overrides carry an undefined value', () => {
660
+ attachHandler();
661
+
662
+ // Overrides diverge from their own stored copy exactly like attributes
663
+ // do: the `undefined`-valued key survives in memory but not in storage.
664
+ faro.api.setSession({
665
+ id: mockSessionId,
666
+ overrides: { serviceName: 'my-service', geoLocationTrackingEnabled: undefined },
667
+ });
668
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(1);
669
+
670
+ faro.api.setView({ name: 'first-view' });
671
+
672
+ expect(storage.storeUserSession).toHaveBeenCalledTimes(1);
673
+ });
674
+
675
+ it('settles when more than one handler is registered', () => {
676
+ // More than one handler can be registered over the same storage. They
677
+ // must not re-trigger one another through the echo notifications of
678
+ // their own writes.
679
+ attachHandler();
680
+ attachHandler();
681
+
682
+ faro.api.setSession({
683
+ id: mockSessionId,
684
+ attributes: { device_battery_level: undefined } as unknown as MetaAttributes,
685
+ });
686
+ // Both handlers may process the original notification, but the writes
687
+ // must settle instead of ping-ponging.
688
+ const writesAfterSettling = storage.storeUserSession.mock.calls.length;
689
+ expect(writesAfterSettling).toBeLessThanOrEqual(2);
690
+
691
+ // ...and a later unrelated meta change must not start writing again.
692
+ faro.api.setView({ name: 'first-view' });
693
+
694
+ expect(storage.storeUserSession.mock.calls.length).toBe(writesAfterSettling);
695
+ });
696
+ });
459
697
  });
460
698
  });
@@ -1,5 +1,15 @@
1
- import { dateNow, deepEqual, EVENT_OVERRIDES_SERVICE_NAME, faro, genShortID, isEmpty } from '@grafana/faro-core';
2
- import type { Meta, MetaOverrides } from '@grafana/faro-core';
1
+ import {
2
+ dateNow,
3
+ deepEqual,
4
+ EVENT_OVERRIDES_SERVICE_NAME,
5
+ faro,
6
+ genShortID,
7
+ isEmpty,
8
+ stringifyExternalJson,
9
+ } from '@grafana/faro-core';
10
+ import type { Meta, MetaOverrides, MetaSession } from '@grafana/faro-core';
11
+
12
+ import { SessionActivityKind } from '../sessionActivity';
3
13
 
4
14
  import { isSampled } from './sampling';
5
15
  import { MAX_SESSION_PERSISTENCE_TIME } from './sessionConstants';
@@ -54,48 +64,126 @@ export function isUserSessionValid(session: FaroUserSession | null): boolean {
54
64
 
55
65
  const { sessionExpirationTime, inactivityTimeout } = getSessionTimeouts();
56
66
  const now = dateNow();
67
+ if (session.started > now || session.lastActivity > now) {
68
+ return false;
69
+ }
70
+
57
71
  const lifetimeValid = now - session.started < sessionExpirationTime;
58
72
 
59
73
  if (!lifetimeValid) {
60
74
  return false;
61
75
  }
62
76
 
77
+ // The session expires at the boundary, matching cold-start record cleanup.
63
78
  const inactivityPeriodValid = now - session.lastActivity < inactivityTimeout;
64
79
  return inactivityPeriodValid;
65
80
  }
66
81
 
67
82
  type GetUserSessionUpdaterParams = {
68
- storeUserSession: (session: FaroUserSession) => void | Promise<void>;
69
- fetchUserSession: () => FaroUserSession | null | Promise<FaroUserSession | null>;
83
+ storeUserSession: (session: FaroUserSession) => void;
84
+ fetchUserSession: () => FaroUserSession | null;
85
+ recordUserSessionActivity?: (session: FaroUserSession) => void;
70
86
  };
71
87
 
72
- export function getUserSessionUpdater({
73
- fetchUserSession,
74
- storeUserSession,
75
- }: GetUserSessionUpdaterParams): () => Promise<void> {
76
- return async function updateSession(): Promise<void> {
77
- if (!fetchUserSession || !storeUserSession) {
78
- return;
79
- }
88
+ type GetUserSessionResetterParams = Pick<GetUserSessionUpdaterParams, 'fetchUserSession' | 'storeUserSession'>;
89
+
90
+ function notifySessionChange(previousSession: FaroUserSession | null, newSession: FaroUserSession): void {
91
+ const newSessionMeta = newSession.sessionMeta;
92
+ if (newSessionMeta == null) {
93
+ return;
94
+ }
95
+
96
+ try {
97
+ faro.config.sessionTracking?.onSessionChange?.(previousSession?.sessionMeta ?? null, newSessionMeta);
98
+ } catch (error) {
99
+ faro.unpatchedConsole?.warn?.('The session change callback failed:', error);
100
+ }
101
+ }
102
+
103
+ function createNextUserSession(previousSession: FaroUserSession | null): FaroUserSession {
104
+ const nextSession = createUserSessionObject({ isSampled: isSampled() });
105
+
106
+ if (previousSession != null && nextSession.sessionId === previousSession.sessionId) {
107
+ faro.unpatchedConsole?.warn?.(
108
+ 'The session ID generator returned the current session ID; using a generated fallback for the new session.'
109
+ );
110
+ nextSession.sessionId = genShortID();
111
+ }
112
+
113
+ return addSessionMetadataToNextSession(nextSession, previousSession);
114
+ }
80
115
 
81
- const sessionFromStorage = await fetchUserSession();
116
+ function startNextUserSession(
117
+ previousSession: FaroUserSession | null,
118
+ storeUserSession: (session: FaroUserSession) => void
119
+ ): FaroUserSession {
120
+ const nextSession = createNextUserSession(previousSession);
82
121
 
83
- if (isUserSessionValid(sessionFromStorage)) {
84
- await storeUserSession({ ...sessionFromStorage!, lastActivity: dateNow() });
85
- } else {
86
- let newSession = addSessionMetadataToNextSession(
87
- createUserSessionObject({ isSampled: isSampled() }),
88
- sessionFromStorage
89
- );
122
+ storeUserSession(nextSession);
123
+ // setSession synchronously emits session_start through the meta listener.
124
+ // The transport hook then resumes and attributes the triggering item to
125
+ // the newly stored session.
126
+ faro.api?.setSession(nextSession.sessionMeta);
127
+ notifySessionChange(previousSession, nextSession);
90
128
 
91
- await storeUserSession(newSession);
129
+ return nextSession;
130
+ }
131
+
132
+ export function getUserSessionResetter({
133
+ fetchUserSession,
134
+ storeUserSession,
135
+ }: GetUserSessionResetterParams): () => FaroUserSession {
136
+ return function resetSession(): FaroUserSession {
137
+ return startNextUserSession(fetchUserSession(), storeUserSession);
138
+ };
139
+ }
92
140
 
93
- faro.api?.setSession(newSession.sessionMeta);
94
- faro.config.sessionTracking?.onSessionChange?.(sessionFromStorage?.sessionMeta ?? null, newSession.sessionMeta!);
141
+ export function getUserSessionUpdater({
142
+ fetchUserSession,
143
+ recordUserSessionActivity,
144
+ storeUserSession,
145
+ }: GetUserSessionUpdaterParams): (
146
+ activity: SessionActivityKind,
147
+ currentSession?: FaroUserSession | null
148
+ ) => FaroUserSession {
149
+ const persistActivity = recordUserSessionActivity ?? storeUserSession;
150
+
151
+ return function updateSession(
152
+ activity: SessionActivityKind,
153
+ currentSession?: FaroUserSession | null
154
+ ): FaroUserSession {
155
+ const sessionFromStorage = currentSession === undefined ? fetchUserSession() : currentSession;
156
+
157
+ if (sessionFromStorage != null && isUserSessionValid(sessionFromStorage)) {
158
+ if (activity === SessionActivityKind.Meaningful) {
159
+ const lastActivity = dateNow();
160
+ if (lastActivity === sessionFromStorage.lastActivity) {
161
+ return sessionFromStorage;
162
+ }
163
+
164
+ const refreshedSession = { ...sessionFromStorage, lastActivity };
165
+ persistActivity(refreshedSession);
166
+ return refreshedSession;
167
+ }
168
+
169
+ return sessionFromStorage;
95
170
  }
171
+
172
+ return startNextUserSession(sessionFromStorage, storeUserSession);
96
173
  };
97
174
  }
98
175
 
176
+ /**
177
+ * Persistent storage serializes sessions with `stringifyExternalJson`, which
178
+ * drops keys whose value is `undefined`. A session meta carrying such a key
179
+ * never compares `deepEqual` to its own stored copy, so the sync handler would
180
+ * rewrite the session on every meta notification. Applying the same round-trip
181
+ * in memory keeps the two sides comparable, for attributes and overrides alike.
182
+ */
183
+ export function toStorableSessionMeta(sessionMeta: MetaSession): MetaSession {
184
+ return JSON.parse(stringifyExternalJson(sessionMeta)) as MetaSession;
185
+ }
186
+
99
187
  export function addSessionMetadataToNextSession(newSession: FaroUserSession, previousSession: FaroUserSession | null) {
100
188
  const sessionWithMeta: Required<FaroUserSession> = {
101
189
  ...newSession,
@@ -115,25 +203,51 @@ export function addSessionMetadataToNextSession(newSession: FaroUserSession, pre
115
203
  }
116
204
 
117
205
  const previousSessionId = previousSession?.sessionId;
118
- if (previousSessionId != null) {
206
+ if (previousSessionId != null && previousSessionId !== newSession.sessionId) {
119
207
  sessionWithMeta.sessionMeta.attributes!['previousSession'] = previousSessionId;
120
208
  }
121
209
 
210
+ // Normalize to what storage can hold, so the meta this session propagates
211
+ // stays comparable to its own stored copy.
212
+ sessionWithMeta.sessionMeta = toStorableSessionMeta(sessionWithMeta.sessionMeta);
213
+
122
214
  return sessionWithMeta;
123
215
  }
124
216
 
125
217
  type GetUserSessionMetaUpdateHandlerParams = {
126
- storeUserSession: (session: FaroUserSession) => void | Promise<void>;
127
- fetchUserSession: () => FaroUserSession | null | Promise<FaroUserSession | null>;
218
+ storeUserSession: (session: FaroUserSession) => void;
219
+ fetchUserSession: () => FaroUserSession | null;
128
220
  };
129
221
 
222
+ /**
223
+ * Set only while a handler applies its own session update.
224
+ *
225
+ * `setSession()` notifies meta listeners synchronously, so a handler is
226
+ * re-entered by its own write. This flag rejects that re-entry, and bounds any
227
+ * future divergence between the in-memory and the stored session to a single
228
+ * redundant write instead of an unbounded loop.
229
+ *
230
+ * Module scoped rather than per handler: more than one handler can be
231
+ * registered over the same storage, and per-handler flags would let them
232
+ * re-trigger one another through the echo notifications of their own writes.
233
+ */
234
+ let isApplyingOwnUpdate = false;
235
+
130
236
  export function getSessionMetaUpdateHandler({
131
237
  fetchUserSession,
132
238
  storeUserSession,
133
239
  }: GetUserSessionMetaUpdateHandlerParams) {
134
- return async function syncSessionIfChangedExternally(meta: Meta) {
135
- const session = meta.session;
136
- const sessionFromSessionStorage = await fetchUserSession();
240
+ return function syncSessionIfChangedExternally(meta: Meta): void {
241
+ // Checked before work begins so the synchronous re-entry triggered by
242
+ // `setSession()` below is rejected while the flag is still set.
243
+ if (isApplyingOwnUpdate) {
244
+ return;
245
+ }
246
+
247
+ // Compare in storable form, so keys that storage cannot hold do not read
248
+ // as a perpetual external change.
249
+ const session = meta.session && toStorableSessionMeta(meta.session);
250
+ const sessionFromSessionStorage = fetchUserSession();
137
251
 
138
252
  let sessionId = session?.id;
139
253
  const sessionAttributes = session?.attributes;
@@ -142,19 +256,46 @@ export function getSessionMetaUpdateHandler({
142
256
  const storedSessionMeta = sessionFromSessionStorage?.sessionMeta;
143
257
  const storedSessionMetaOverrides = storedSessionMeta?.overrides;
144
258
 
145
- const hasSessionOverridesChanged = !!sessionOverrides && !deepEqual(sessionOverrides, storedSessionMetaOverrides);
146
- const hasAttributesChanged = !!sessionAttributes && !deepEqual(sessionAttributes, storedSessionMeta?.attributes);
259
+ const hasSessionOverridesChanged =
260
+ sessionOverrides != null && !deepEqual(sessionOverrides, storedSessionMetaOverrides);
261
+ const hasSessionMetaChanged =
262
+ session != null &&
263
+ (!deepEqual(sessionAttributes, storedSessionMeta?.attributes) ||
264
+ !deepEqual(sessionOverrides, storedSessionMetaOverrides));
147
265
  const hasSessionIdChanged = !!session && sessionId !== sessionFromSessionStorage?.sessionId;
148
266
 
149
- if (hasSessionIdChanged || hasAttributesChanged || hasSessionOverridesChanged) {
150
- const userSession = addSessionMetadataToNextSession(
151
- createUserSessionObject({ sessionId, isSampled: isSampled() }),
152
- sessionFromSessionStorage
153
- );
154
-
155
- await storeUserSession(userSession);
267
+ if (hasSessionIdChanged || hasSessionMetaChanged) {
268
+ const storedPreviousSessionId = storedSessionMeta?.attributes?.['previousSession'];
269
+ const effectiveOverrides = sessionOverrides ?? storedSessionMetaOverrides;
270
+ const userSession =
271
+ sessionFromSessionStorage != null && !hasSessionIdChanged
272
+ ? {
273
+ ...sessionFromSessionStorage,
274
+ sessionMeta: toStorableSessionMeta({
275
+ id: sessionFromSessionStorage.sessionId,
276
+ attributes: {
277
+ ...faro.config.sessionTracking?.session?.attributes,
278
+ ...(sessionAttributes ?? storedSessionMeta?.attributes),
279
+ ...(storedPreviousSessionId == null ? {} : { previousSession: storedPreviousSessionId }),
280
+ isSampled: sessionFromSessionStorage.isSampled.toString(),
281
+ },
282
+ ...(isEmpty(effectiveOverrides) ? {} : { overrides: effectiveOverrides }),
283
+ }),
284
+ }
285
+ : addSessionMetadataToNextSession(
286
+ createUserSessionObject({ sessionId, isSampled: isSampled() }),
287
+ sessionFromSessionStorage
288
+ );
289
+
290
+ storeUserSession(userSession);
156
291
  sendOverrideEvent(hasSessionOverridesChanged, sessionOverrides, storedSessionMetaOverrides);
157
- faro.api.setSession(userSession.sessionMeta);
292
+
293
+ isApplyingOwnUpdate = true;
294
+ try {
295
+ faro.api.setSession(userSession.sessionMeta);
296
+ } finally {
297
+ isApplyingOwnUpdate = false;
298
+ }
158
299
  }
159
300
  };
160
301
  }
@@ -1,5 +1,7 @@
1
1
  import type { MetaSession } from '@grafana/faro-core';
2
2
 
3
+ import type { SessionActivityKind } from '../sessionActivity';
4
+
3
5
  import type { MmkvPersistentSessionsManager } from './MmkvPersistentSessionsManager';
4
6
  import type { VolatileSessionsManager } from './VolatileSessionManager';
5
7
 
@@ -12,3 +14,9 @@ export interface FaroUserSession {
12
14
  }
13
15
 
14
16
  export type SessionManager = typeof VolatileSessionsManager | typeof MmkvPersistentSessionsManager;
17
+
18
+ export interface SessionManagerInstance {
19
+ checkSession(activity: SessionActivityKind, currentSession?: FaroUserSession | null): FaroUserSession;
20
+ resetSession(): FaroUserSession;
21
+ unpatch(): void;
22
+ }