@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.
- package/README.md +176 -30
- package/android/build.gradle +17 -12
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
- package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
- package/dist/cjs/config/getRNInstrumentations.js +6 -8
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/sampling.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/appState/index.js +8 -2
- package/dist/cjs/instrumentations/appState/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +8 -3
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/cjs/instrumentations/performance/index.js +2 -6
- package/dist/cjs/instrumentations/performance/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/index.js +214 -45
- package/dist/cjs/instrumentations/session/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
- package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
- package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
- package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/cjs/instrumentations/startup/index.js +2 -6
- package/dist/cjs/instrumentations/startup/index.js.map +1 -1
- package/dist/cjs/navigation/utils.js +3 -1
- package/dist/cjs/navigation/utils.js.map +1 -1
- package/dist/cjs/transports/fetch/index.js.map +1 -1
- package/dist/cjs/transports/fetch/transport.js +56 -25
- package/dist/cjs/transports/fetch/transport.js.map +1 -1
- package/dist/cjs/transports/fetch/types.js.map +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/cjs/transports/offline/OfflineCache.js +2 -2
- package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
- package/dist/cjs/transports/offline/transport.js +3 -4
- package/dist/cjs/transports/offline/transport.js.map +1 -1
- package/dist/cjs/userPersistence/UserPersistence.js +1 -2
- package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +6 -8
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/sampling.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
- package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/appState/index.js +8 -2
- package/dist/esm/instrumentations/appState/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +8 -3
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/esm/instrumentations/performance/index.js +2 -6
- package/dist/esm/instrumentations/performance/index.js.map +1 -1
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/index.js +207 -46
- package/dist/esm/instrumentations/session/index.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
- package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
- package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
- package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/esm/instrumentations/startup/index.js +2 -6
- package/dist/esm/instrumentations/startup/index.js.map +1 -1
- package/dist/esm/navigation/utils.js +2 -1
- package/dist/esm/navigation/utils.js.map +1 -1
- package/dist/esm/transports/fetch/index.js.map +1 -1
- package/dist/esm/transports/fetch/transport.js +38 -14
- package/dist/esm/transports/fetch/transport.js.map +1 -1
- package/dist/esm/transports/fetch/types.js.map +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/esm/transports/offline/OfflineCache.js +2 -2
- package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
- package/dist/esm/transports/offline/transport.js +3 -4
- package/dist/esm/transports/offline/transport.js.map +1 -1
- package/dist/esm/userPersistence/UserPersistence.js +1 -2
- package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
- package/dist/types/config/getRNInstrumentations.d.ts +1 -2
- package/dist/types/config/sampling.d.ts +0 -3
- package/dist/types/config/types.d.ts +5 -3
- package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/performance/index.d.ts +0 -2
- package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
- package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
- package/dist/types/instrumentations/session/index.d.ts +20 -1
- package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
- package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
- package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
- package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
- package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
- package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
- package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
- package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
- package/dist/types/instrumentations/startup/index.d.ts +2 -6
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +8 -1
- package/dist/types/transports/fetch/types.d.ts +5 -0
- package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
- package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
- package/dist/types/transports/offline/transport.d.ts +3 -4
- package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
- package/ios/FaroCrashReporter.swift +214 -21
- package/ios/FaroReactNative.swift +24 -0
- package/ios/FaroReactNativeModule.h +2 -1
- package/ios/FaroReactNativeModule.mm +121 -6
- package/ios/RefreshRateVitals.swift +34 -38
- package/package.json +7 -5
- package/src/config/getRNInstrumentations.ts +6 -8
- package/src/config/makeRNConfig.test.ts +6 -0
- package/src/config/sampling.ts +0 -3
- package/src/config/types.ts +5 -3
- package/src/dataCollection/DataCollectionPolicy.ts +1 -2
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/index.ts +8 -2
- package/src/instrumentations/anr/instrumentation.ts +8 -1
- package/src/instrumentations/appState/index.ts +10 -2
- package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
- package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
- package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
- package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/types.ts +7 -0
- package/src/instrumentations/errors/index.ts +12 -4
- package/src/instrumentations/errors/instrumentation.test.ts +35 -0
- package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
- package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
- package/src/instrumentations/frameMonitoring/types.ts +4 -6
- package/src/instrumentations/performance/index.ts +2 -6
- package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
- package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
- package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
- package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
- package/src/instrumentations/session/directSessionActivity.ts +36 -0
- package/src/instrumentations/session/index.ts +269 -46
- package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
- package/src/instrumentations/session/sessionActivity.test.ts +68 -0
- package/src/instrumentations/session/sessionActivity.ts +58 -0
- package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
- package/src/instrumentations/session/sessionAttributes.ts +12 -6
- package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
- package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
- package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
- package/src/instrumentations/session/sessionManager/types.ts +8 -0
- package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
- package/src/instrumentations/session/sessionProcess.test.ts +136 -0
- package/src/instrumentations/session/sessionProcess.ts +114 -0
- package/src/instrumentations/session/startNewSession.test.ts +337 -0
- package/src/instrumentations/startup/index.ts +2 -6
- package/src/navigation/utils.ts +3 -1
- package/src/transports/fetch/index.ts +6 -1
- package/src/transports/fetch/transport.test.ts +86 -2
- package/src/transports/fetch/transport.ts +38 -16
- package/src/transports/fetch/types.ts +7 -0
- package/src/transports/offline/ConnectivityService.ts +1 -1
- package/src/transports/offline/OfflineCache.ts +2 -2
- package/src/transports/offline/transport.ts +3 -4
- package/src/userPersistence/UserPersistence.ts +1 -2
- package/dist/cjs/utils/throttle.js +0 -42
- package/dist/cjs/utils/throttle.js.map +0 -1
- package/dist/esm/utils/throttle.js +0 -34
- package/dist/esm/utils/throttle.js.map +0 -1
- package/dist/types/utils/throttle.d.ts +0 -9
- package/src/utils/throttle.test.ts +0 -141
- 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().
|
|
299
|
-
const mockStoreUserSession = jest.fn()
|
|
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
|
-
|
|
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().
|
|
333
|
-
const mockStoreUserSession = jest.fn()
|
|
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
|
-
|
|
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().
|
|
354
|
-
const mockStoreUserSession = jest.fn()
|
|
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(
|
|
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:
|
|
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: '
|
|
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().
|
|
395
|
-
const mockStoreUserSession = jest.fn()
|
|
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().
|
|
435
|
-
const mockStoreUserSession = jest.fn()
|
|
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 {
|
|
2
|
-
|
|
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
|
|
69
|
-
fetchUserSession: () => FaroUserSession | null
|
|
83
|
+
storeUserSession: (session: FaroUserSession) => void;
|
|
84
|
+
fetchUserSession: () => FaroUserSession | null;
|
|
85
|
+
recordUserSessionActivity?: (session: FaroUserSession) => void;
|
|
70
86
|
};
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
116
|
+
function startNextUserSession(
|
|
117
|
+
previousSession: FaroUserSession | null,
|
|
118
|
+
storeUserSession: (session: FaroUserSession) => void
|
|
119
|
+
): FaroUserSession {
|
|
120
|
+
const nextSession = createNextUserSession(previousSession);
|
|
82
121
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
127
|
-
fetchUserSession: () => 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
|
|
135
|
-
|
|
136
|
-
|
|
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 =
|
|
146
|
-
|
|
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 ||
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
+
}
|