@grafana/faro-react-native 1.3.0 → 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 +20 -15
- 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/anr/instrumentation.d.ts +1 -1
- package/dist/types/instrumentations/appState/index.d.ts +1 -1
- package/dist/types/instrumentations/console/index.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/http/index.d.ts +1 -1
- package/dist/types/instrumentations/performance/index.d.ts +1 -3
- 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 +21 -2
- 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 +3 -7
- package/dist/types/instrumentations/userActions/index.d.ts +1 -1
- package/dist/types/instrumentations/view/index.d.ts +1 -1
- package/dist/types/instrumentations/xhr/index.d.ts +1 -1
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
- package/dist/types/transports/console/transport.d.ts +1 -1
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +9 -2
- 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 +4 -5
- 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 +8 -6
- 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,26 +1,135 @@
|
|
|
1
|
-
import { BaseInstrumentation, dateNow, EVENT_SESSION_START,
|
|
2
|
-
import type {
|
|
1
|
+
import { BaseInstrumentation, dateNow, EVENT_SESSION_START, faro, VERSION } from '@grafana/faro-core';
|
|
2
|
+
import type {
|
|
3
|
+
API,
|
|
4
|
+
BeforeSendHook,
|
|
5
|
+
Config,
|
|
6
|
+
Meta,
|
|
7
|
+
Metas,
|
|
8
|
+
MetaSession,
|
|
9
|
+
TransportItem,
|
|
10
|
+
Transports,
|
|
11
|
+
UserActionInternalInterface,
|
|
12
|
+
} from '@grafana/faro-core';
|
|
3
13
|
|
|
4
14
|
import type { ReactNativeFullConfig, ReactNativeSessionTrackingConfig } from '../../config/types';
|
|
5
15
|
|
|
16
|
+
import { clearDirectSessionActivityHandler, registerDirectSessionActivityHandler } from './directSessionActivity';
|
|
17
|
+
import { classifySessionActivity, isRecoveredCrashItem, SessionActivityKind } from './sessionActivity';
|
|
6
18
|
import { minimalSessionDeviceAttributes, type SessionAttributes } from './sessionAttributes';
|
|
7
19
|
import { type FaroUserSession, getSessionManagerByConfig, isSampled } from './sessionManager';
|
|
8
20
|
import { MAX_SESSION_PERSISTENCE_TIME } from './sessionManager/sessionConstants';
|
|
9
21
|
import { createUserSessionObject, isUserSessionValid } from './sessionManager/sessionManagerUtils';
|
|
10
|
-
import type { SessionManager } from './sessionManager/types';
|
|
22
|
+
import type { SessionManager, SessionManagerInstance } from './sessionManager/types';
|
|
23
|
+
|
|
24
|
+
const SESSION_INSTRUMENTATION_NAME = '@grafana/faro-react-native:instrumentation-session';
|
|
25
|
+
|
|
26
|
+
// Track the current session hook per transport so superseded hooks yield to a
|
|
27
|
+
// replacement. The shared fallback preserves sampling decisions captured on
|
|
28
|
+
// queued items after the owning instrumentation has detached.
|
|
29
|
+
const activeSessionBeforeSendHooks = new WeakMap<Transports, BeforeSendHook>();
|
|
30
|
+
const sessionSamplingFallbackHooks = new WeakMap<Transports, BeforeSendHook>();
|
|
31
|
+
|
|
32
|
+
function applySessionSampling(item: TransportItem, fallbackSamplingDecision?: boolean): TransportItem | null {
|
|
33
|
+
const attributes = item.meta.session?.attributes;
|
|
34
|
+
const samplingAttribute = attributes?.['isSampled'] ?? fallbackSamplingDecision?.toString();
|
|
35
|
+
|
|
36
|
+
if (samplingAttribute === 'false') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (samplingAttribute !== 'true') {
|
|
41
|
+
return item;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sampledItem: TransportItem = {
|
|
45
|
+
...item,
|
|
46
|
+
meta: {
|
|
47
|
+
...item.meta,
|
|
48
|
+
session: item.meta.session
|
|
49
|
+
? {
|
|
50
|
+
...item.meta.session,
|
|
51
|
+
attributes: item.meta.session.attributes ? { ...item.meta.session.attributes } : undefined,
|
|
52
|
+
}
|
|
53
|
+
: undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const sampledAttributes = sampledItem.meta.session?.attributes;
|
|
58
|
+
delete sampledAttributes?.['isSampled'];
|
|
59
|
+
|
|
60
|
+
if (Object.keys(sampledAttributes ?? {}).length === 0) {
|
|
61
|
+
delete sampledItem.meta.session?.attributes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return sampledItem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureSessionSamplingFallback(transports: Transports): void {
|
|
68
|
+
if (sessionSamplingFallbackHooks.has(transports)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fallbackHook: BeforeSendHook = (item) =>
|
|
73
|
+
activeSessionBeforeSendHooks.has(transports) ? item : applySessionSampling(item);
|
|
74
|
+
|
|
75
|
+
transports.addBeforeSendHooks(fallbackHook);
|
|
76
|
+
sessionSamplingFallbackHooks.set(transports, fallbackHook);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeInternalSamplingAttribute(session: MetaSession | undefined): MetaSession | undefined {
|
|
80
|
+
if (session?.attributes?.['isSampled'] == null) {
|
|
81
|
+
return session;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const attributes = { ...session.attributes };
|
|
85
|
+
delete attributes['isSampled'];
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...session,
|
|
89
|
+
attributes: Object.keys(attributes).length === 0 ? undefined : attributes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Starts a new linked session at an application-defined boundary.
|
|
95
|
+
*
|
|
96
|
+
* Call this after changing or clearing the current user for logout, account
|
|
97
|
+
* changes, or another boundary that must not share a session. Calls made
|
|
98
|
+
* before Faro initializes, after teardown, or while session tracking is
|
|
99
|
+
* disabled have no effect.
|
|
100
|
+
*/
|
|
101
|
+
export function startNewSession(): void {
|
|
102
|
+
const instrumentation = faro?.instrumentations?.instrumentations.find(
|
|
103
|
+
({ name }) => name === SESSION_INSTRUMENTATION_NAME
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
instrumentation != null &&
|
|
108
|
+
'startNewSession' in instrumentation &&
|
|
109
|
+
typeof instrumentation.startNewSession === 'function'
|
|
110
|
+
) {
|
|
111
|
+
instrumentation.startNewSession();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
11
114
|
|
|
12
115
|
/**
|
|
13
116
|
* Session instrumentation for React Native
|
|
14
117
|
* Manages persistent or volatile sessions with expiration and inactivity tracking
|
|
15
118
|
*/
|
|
16
119
|
export class SessionInstrumentation extends BaseInstrumentation {
|
|
17
|
-
readonly name =
|
|
120
|
+
readonly name = SESSION_INSTRUMENTATION_NAME;
|
|
18
121
|
readonly version = VERSION;
|
|
19
122
|
|
|
20
123
|
// previously notified session, to ensure we don't send session start
|
|
21
124
|
// event twice for the same session
|
|
22
125
|
private notifiedSession: MetaSession | undefined;
|
|
23
|
-
private sessionManagerInstance:
|
|
126
|
+
private sessionManagerInstance: SessionManagerInstance | undefined;
|
|
127
|
+
private isResettingSession = false;
|
|
128
|
+
private unregisterDirectSessionActivity: (() => void) | undefined;
|
|
129
|
+
private sessionMetaListenerMetas: Metas | undefined;
|
|
130
|
+
private beforeSendHookApi: API | undefined;
|
|
131
|
+
private beforeSendHook: BeforeSendHook | undefined;
|
|
132
|
+
private beforeSendHookTransports: Transports | undefined;
|
|
24
133
|
|
|
25
134
|
private getDefaultSessionDeviceAttributes(): SessionAttributes {
|
|
26
135
|
const cfg = this.config as ReactNativeFullConfig;
|
|
@@ -30,7 +139,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
30
139
|
return minimalSessionDeviceAttributes();
|
|
31
140
|
}
|
|
32
141
|
|
|
33
|
-
private sendSessionStartEvent(meta: Meta): void {
|
|
142
|
+
private readonly sendSessionStartEvent = (meta: Meta): void => {
|
|
34
143
|
const session = meta.session;
|
|
35
144
|
|
|
36
145
|
if (session && session.id !== this.notifiedSession?.id) {
|
|
@@ -39,6 +148,11 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
39
148
|
// automatically
|
|
40
149
|
this.api.pushEvent(EVENT_SESSION_START, {}, undefined, { skipDedupe: true });
|
|
41
150
|
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
private registerSessionMetaListener(): void {
|
|
154
|
+
this.metas.addListener(this.sendSessionStartEvent);
|
|
155
|
+
this.sessionMetaListenerMetas = this.metas;
|
|
42
156
|
}
|
|
43
157
|
|
|
44
158
|
private createInitialSession(
|
|
@@ -55,7 +169,10 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
55
169
|
|
|
56
170
|
if (sessionsConfig.persistent && storedUserSession) {
|
|
57
171
|
const now = dateNow();
|
|
58
|
-
const shouldClearPersistentSession =
|
|
172
|
+
const shouldClearPersistentSession =
|
|
173
|
+
storedUserSession.started > now ||
|
|
174
|
+
storedUserSession.lastActivity > now ||
|
|
175
|
+
storedUserSession.lastActivity <= now - maxPersistenceMs;
|
|
59
176
|
|
|
60
177
|
if (shouldClearPersistentSession) {
|
|
61
178
|
SessionManagerClass.removeUserSession();
|
|
@@ -68,7 +185,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
68
185
|
let emitSessionStartOnInit: boolean;
|
|
69
186
|
let initialSession: FaroUserSession;
|
|
70
187
|
|
|
71
|
-
if (isUserSessionValid(storedUserSession)) {
|
|
188
|
+
if (!sessionsConfig.persistent && isUserSessionValid(storedUserSession)) {
|
|
72
189
|
const sessionId = storedUserSession?.sessionId;
|
|
73
190
|
|
|
74
191
|
initialSession = createUserSessionObject({
|
|
@@ -91,7 +208,7 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
91
208
|
...sessionsConfig.session?.attributes,
|
|
92
209
|
// Merge with stored attributes
|
|
93
210
|
...storedUserSessionMeta?.attributes,
|
|
94
|
-
// Default attributes take precedence
|
|
211
|
+
// Default attributes take precedence
|
|
95
212
|
...defaultAttributes,
|
|
96
213
|
// For valid resumed sessions we do not want to recalculate the sampling decision on each init phase.
|
|
97
214
|
isSampled: initialSession.isSampled.toString(),
|
|
@@ -101,23 +218,24 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
101
218
|
|
|
102
219
|
emitSessionStartOnInit = false;
|
|
103
220
|
} else {
|
|
104
|
-
const sessionId = sessionsConfig.session?.id ?? genShortID();
|
|
105
|
-
|
|
106
221
|
initialSession = createUserSessionObject({
|
|
107
|
-
sessionId,
|
|
222
|
+
sessionId: sessionsConfig.session?.id,
|
|
108
223
|
isSampled: isSampled(),
|
|
109
224
|
});
|
|
110
225
|
|
|
226
|
+
const sessionId = initialSession.sessionId;
|
|
227
|
+
const previousSessionId = storedUserSession?.sessionId === sessionId ? undefined : storedUserSession?.sessionId;
|
|
111
228
|
const overrides = sessionsConfig.session?.overrides;
|
|
112
229
|
|
|
113
230
|
initialSession.sessionMeta = {
|
|
114
231
|
id: sessionId,
|
|
115
232
|
attributes: {
|
|
116
|
-
isSampled: initialSession.isSampled.toString(),
|
|
117
233
|
// Start with custom attributes from config
|
|
118
234
|
...sessionsConfig.session?.attributes,
|
|
119
|
-
// Default attributes take precedence
|
|
235
|
+
// Default attributes take precedence
|
|
120
236
|
...defaultAttributes,
|
|
237
|
+
isSampled: initialSession.isSampled.toString(),
|
|
238
|
+
...(previousSessionId == null ? {} : { previousSession: previousSessionId }),
|
|
121
239
|
},
|
|
122
240
|
// new session we don't care about previous overrides
|
|
123
241
|
...(overrides ? { overrides } : {}),
|
|
@@ -129,51 +247,80 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
129
247
|
return { initialSession, emitSessionStartOnInit };
|
|
130
248
|
}
|
|
131
249
|
|
|
132
|
-
private registerBeforeSendHook(SessionManagerClass: SessionManager) {
|
|
133
|
-
const
|
|
250
|
+
private registerBeforeSendHook(SessionManagerClass: SessionManager): void {
|
|
251
|
+
const registeredTransports = this.transports;
|
|
252
|
+
ensureSessionSamplingFallback(registeredTransports);
|
|
134
253
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Only filter out items when session is explicitly NOT sampled (isSampled='false')
|
|
141
|
-
// If isSampled='true', remove the attribute before sending (it's internal)
|
|
142
|
-
// If no isSampled attribute, pass through the item unchanged
|
|
143
|
-
if (attributes?.['isSampled'] === 'false') {
|
|
144
|
-
// Session is not sampled - drop this item
|
|
145
|
-
return null;
|
|
254
|
+
const beforeSendHook: BeforeSendHook = (item: TransportItem) => {
|
|
255
|
+
const activeBeforeSendHook = activeSessionBeforeSendHooks.get(registeredTransports);
|
|
256
|
+
if (activeBeforeSendHook != null && activeBeforeSendHook !== beforeSendHook) {
|
|
257
|
+
return item;
|
|
146
258
|
}
|
|
147
259
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
260
|
+
let nextItem = item;
|
|
261
|
+
let fallbackSamplingDecision: boolean | undefined;
|
|
262
|
+
const sessionManager = this.beforeSendHook === beforeSendHook ? this.sessionManagerInstance : undefined;
|
|
263
|
+
|
|
264
|
+
if (sessionManager != null) {
|
|
265
|
+
const storedSession = SessionManagerClass.fetchUserSession();
|
|
266
|
+
const recoveredCrash = isRecoveredCrashItem(item, storedSession?.sessionId);
|
|
267
|
+
// Ending an active action can send telemetry synchronously. Keep that
|
|
268
|
+
// telemetry on the current session until the explicit reset completes.
|
|
269
|
+
const checkedSession = recoveredCrash
|
|
270
|
+
? null
|
|
271
|
+
: this.isResettingSession
|
|
272
|
+
? storedSession
|
|
273
|
+
: sessionManager.checkSession(classifySessionActivity(item), storedSession);
|
|
274
|
+
|
|
275
|
+
if (checkedSession?.sessionMeta != null && item.meta.session?.id !== checkedSession.sessionId) {
|
|
276
|
+
nextItem = {
|
|
277
|
+
...item,
|
|
278
|
+
meta: {
|
|
279
|
+
...item.meta,
|
|
280
|
+
session: checkedSession.sessionMeta,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
157
283
|
}
|
|
158
284
|
|
|
159
|
-
|
|
285
|
+
// New crash records carry the crash-time decision. Older records may not,
|
|
286
|
+
// so retain the previous behavior and use the live session's stable
|
|
287
|
+
// decision rather than implicitly sampling every recovered crash.
|
|
288
|
+
fallbackSamplingDecision = recoveredCrash ? storedSession?.isSampled : checkedSession?.isSampled;
|
|
160
289
|
}
|
|
161
290
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
291
|
+
return applySessionSampling(nextItem, fallbackSamplingDecision);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.beforeSendHook = beforeSendHook;
|
|
295
|
+
this.beforeSendHookApi = this.api;
|
|
296
|
+
this.beforeSendHookTransports = registeredTransports;
|
|
297
|
+
registeredTransports.addBeforeSendHooks(beforeSendHook);
|
|
298
|
+
activeSessionBeforeSendHooks.set(registeredTransports, beforeSendHook);
|
|
165
299
|
}
|
|
166
300
|
|
|
167
301
|
initialize(): void {
|
|
302
|
+
// BaseInstrumentation instances can be registered more than once. Detach
|
|
303
|
+
// every resource from the previous Faro instance before binding the next.
|
|
304
|
+
this.unpatch();
|
|
305
|
+
|
|
168
306
|
const sessionTrackingConfig = this.config.sessionTracking;
|
|
169
307
|
|
|
308
|
+
// A new initialization owns direct-activity routing, including when
|
|
309
|
+
// session tracking is disabled.
|
|
310
|
+
clearDirectSessionActivityHandler();
|
|
311
|
+
|
|
170
312
|
if (!sessionTrackingConfig?.enabled) {
|
|
171
|
-
this.
|
|
313
|
+
this.registerSessionMetaListener();
|
|
172
314
|
return;
|
|
173
315
|
}
|
|
174
316
|
|
|
175
317
|
const SessionManagerClass = getSessionManagerByConfig(sessionTrackingConfig);
|
|
176
318
|
|
|
319
|
+
// Constructing a manager registers an AppState subscription and a metas
|
|
320
|
+
// listener, so exactly one instance is created and reused. It is also the
|
|
321
|
+
// instance `unpatch()` cleans up.
|
|
322
|
+
this.sessionManagerInstance = new SessionManagerClass(this.metas);
|
|
323
|
+
|
|
177
324
|
this.registerBeforeSendHook(SessionManagerClass);
|
|
178
325
|
|
|
179
326
|
const { initialSession, emitSessionStartOnInit } = this.createInitialSession(
|
|
@@ -186,21 +333,97 @@ export class SessionInstrumentation extends BaseInstrumentation {
|
|
|
186
333
|
this.notifiedSession = initialSessionMeta;
|
|
187
334
|
this.api.setSession(initialSessionMeta);
|
|
188
335
|
|
|
189
|
-
this.sessionManagerInstance = new SessionManagerClass();
|
|
190
|
-
|
|
191
336
|
if (emitSessionStartOnInit) {
|
|
192
337
|
this.api.pushEvent(EVENT_SESSION_START, {}, undefined, { skipDedupe: true });
|
|
193
338
|
}
|
|
194
339
|
|
|
195
|
-
this.
|
|
340
|
+
this.unregisterDirectSessionActivity = registerDirectSessionActivityHandler(() => {
|
|
341
|
+
this.sessionManagerInstance?.checkSession(SessionActivityKind.Meaningful);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
this.registerSessionMetaListener();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Starts a new linked session immediately. */
|
|
348
|
+
startNewSession(): void {
|
|
349
|
+
if (this.sessionManagerInstance == null || this.isResettingSession) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.isResettingSession = true;
|
|
354
|
+
try {
|
|
355
|
+
try {
|
|
356
|
+
const activeAction = this.api.getActiveUserAction?.() as UserActionInternalInterface | undefined;
|
|
357
|
+
activeAction?.end();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.logWarn('Failed to end the active user action before resetting the session:', error);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.sessionManagerInstance.resetSession();
|
|
363
|
+
} finally {
|
|
364
|
+
this.isResettingSession = false;
|
|
365
|
+
}
|
|
196
366
|
}
|
|
197
367
|
|
|
198
368
|
/**
|
|
199
369
|
* Clean up session manager listeners
|
|
200
370
|
*/
|
|
201
371
|
unpatch(): void {
|
|
202
|
-
|
|
203
|
-
|
|
372
|
+
this.unregisterDirectSessionActivity?.();
|
|
373
|
+
this.unregisterDirectSessionActivity = undefined;
|
|
374
|
+
|
|
375
|
+
const beforeSendHook = this.beforeSendHook;
|
|
376
|
+
const beforeSendHookApi = this.beforeSendHookApi;
|
|
377
|
+
const beforeSendHookTransports = this.beforeSendHookTransports;
|
|
378
|
+
const ownedActiveHook =
|
|
379
|
+
beforeSendHook != null &&
|
|
380
|
+
beforeSendHookTransports != null &&
|
|
381
|
+
activeSessionBeforeSendHooks.get(beforeSendHookTransports) === beforeSendHook;
|
|
382
|
+
|
|
383
|
+
// Stop the manager's metas synchronization before removing the internal
|
|
384
|
+
// sampling marker. Keep the session hook active during that update so any
|
|
385
|
+
// surviving metas listener that emits telemetry is still attributed and
|
|
386
|
+
// filtered with the current session.
|
|
387
|
+
this.sessionManagerInstance?.unpatch();
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
if (ownedActiveHook && beforeSendHookApi) {
|
|
391
|
+
const session = beforeSendHookApi.getSession();
|
|
392
|
+
const sessionWithoutSamplingAttribute = removeInternalSamplingAttribute(session);
|
|
393
|
+
if (sessionWithoutSamplingAttribute !== session) {
|
|
394
|
+
this.isResettingSession = true;
|
|
395
|
+
try {
|
|
396
|
+
beforeSendHookApi.setSession(sessionWithoutSamplingAttribute);
|
|
397
|
+
} finally {
|
|
398
|
+
this.isResettingSession = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} finally {
|
|
403
|
+
this.beforeSendHook = undefined;
|
|
404
|
+
this.beforeSendHookApi = undefined;
|
|
405
|
+
this.beforeSendHookTransports = undefined;
|
|
406
|
+
if (beforeSendHook && beforeSendHookTransports) {
|
|
407
|
+
if (ownedActiveHook) {
|
|
408
|
+
activeSessionBeforeSendHooks.delete(beforeSendHookTransports);
|
|
409
|
+
}
|
|
410
|
+
beforeSendHookTransports.removeBeforeSendHooks(beforeSendHook);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.sessionManagerInstance = undefined;
|
|
414
|
+
|
|
415
|
+
const sessionMetaListenerMetas = this.sessionMetaListenerMetas;
|
|
416
|
+
this.sessionMetaListenerMetas = undefined;
|
|
417
|
+
if (sessionMetaListenerMetas) {
|
|
418
|
+
sessionMetaListenerMetas.removeListener(this.sendSessionStartEvent);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.notifiedSession = undefined;
|
|
422
|
+
this.isResettingSession = false;
|
|
204
423
|
}
|
|
205
424
|
}
|
|
425
|
+
|
|
426
|
+
destroy(): void {
|
|
427
|
+
this.unpatch();
|
|
428
|
+
}
|
|
206
429
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { EVENT_SESSION_START, initializeFaro } from '@grafana/faro-core';
|
|
4
|
+
import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
|
|
5
|
+
|
|
6
|
+
import { SessionInstrumentation } from './index';
|
|
7
|
+
import {
|
|
8
|
+
MmkvPersistentSessionsManager,
|
|
9
|
+
resetMmkvSingletonForTests,
|
|
10
|
+
} from './sessionManager/MmkvPersistentSessionsManager';
|
|
11
|
+
import { MAX_SESSION_PERSISTENCE_TIME, STORAGE_KEY } from './sessionManager/sessionConstants';
|
|
12
|
+
import { resetSessionProcessForTests } from './sessionProcess';
|
|
13
|
+
|
|
14
|
+
const mockMmkvValues = new Map<string, string>();
|
|
15
|
+
const mockMmkv = {
|
|
16
|
+
getString: jest.fn((key: string) => mockMmkvValues.get(key)),
|
|
17
|
+
set: jest.fn((key: string, value: string) => mockMmkvValues.set(key, value)),
|
|
18
|
+
remove: jest.fn((key: string) => mockMmkvValues.delete(key)),
|
|
19
|
+
};
|
|
20
|
+
const mockCreateMMKV = jest.fn(() => mockMmkv);
|
|
21
|
+
const originalNativeModule = NativeModules.FaroReactNativeModule;
|
|
22
|
+
|
|
23
|
+
jest.mock('react-native-mmkv', () => ({
|
|
24
|
+
createMMKV: mockCreateMMKV,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const now = new Date('2026-08-14T12:00:00.000Z').getTime();
|
|
28
|
+
|
|
29
|
+
function persistedSession({
|
|
30
|
+
currentSessionId = 'persisted-session',
|
|
31
|
+
startedAt = now - 10_000,
|
|
32
|
+
lastActivityAt = now - 1,
|
|
33
|
+
}: {
|
|
34
|
+
currentSessionId?: string;
|
|
35
|
+
startedAt?: number;
|
|
36
|
+
lastActivityAt?: number;
|
|
37
|
+
} = {}) {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
currentSessionId,
|
|
41
|
+
previousSessionId: 'older-session',
|
|
42
|
+
startedAt,
|
|
43
|
+
lastActivityAt,
|
|
44
|
+
isSampled: false,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function initializePersistentSession() {
|
|
49
|
+
const transport = new MockTransport();
|
|
50
|
+
const faro = initializeFaro(
|
|
51
|
+
mockConfig({
|
|
52
|
+
transports: [transport],
|
|
53
|
+
instrumentations: [new SessionInstrumentation()],
|
|
54
|
+
sessionTracking: {
|
|
55
|
+
enabled: true,
|
|
56
|
+
persistent: true,
|
|
57
|
+
generateSessionId: () => 'new-session',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { faro, transport };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('persistent session cold starts', () => {
|
|
66
|
+
beforeAll(() => {
|
|
67
|
+
jest.useFakeTimers();
|
|
68
|
+
jest.setSystemTime(now);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
NativeModules.FaroReactNativeModule = {
|
|
73
|
+
claimSessionPersistence: () => true,
|
|
74
|
+
getSessionProcessIdentifier: () => 'com.example.myapp',
|
|
75
|
+
isMainSessionProcess: () => true,
|
|
76
|
+
releaseSessionPersistence: () => true,
|
|
77
|
+
};
|
|
78
|
+
jest.clearAllMocks();
|
|
79
|
+
mockMmkvValues.clear();
|
|
80
|
+
resetMmkvSingletonForTests();
|
|
81
|
+
resetSessionProcessForTests();
|
|
82
|
+
delete (globalThis as Record<string, unknown>)['faro'];
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterAll(() => {
|
|
86
|
+
NativeModules.FaroReactNativeModule = originalNativeModule;
|
|
87
|
+
resetSessionProcessForTests();
|
|
88
|
+
jest.useRealTimers();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('starts an unlinked session when stored state is missing', () => {
|
|
92
|
+
const { faro } = initializePersistentSession();
|
|
93
|
+
|
|
94
|
+
expect(faro.metas.value.session?.id).toBe('new-session');
|
|
95
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('creates a new session linked to a valid persisted session', () => {
|
|
99
|
+
mockMmkvValues.set(STORAGE_KEY, persistedSession());
|
|
100
|
+
|
|
101
|
+
const { faro, transport } = initializePersistentSession();
|
|
102
|
+
|
|
103
|
+
expect(faro.metas.value.session?.id).toBe('new-session');
|
|
104
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBe('persisted-session');
|
|
105
|
+
const sessionStartEvents = transport.items.filter(
|
|
106
|
+
(item) => item.type === 'event' && 'name' in item.payload && item.payload.name === EVENT_SESSION_START
|
|
107
|
+
);
|
|
108
|
+
expect(sessionStartEvents).toHaveLength(1);
|
|
109
|
+
expect(sessionStartEvents[0]?.meta.session?.attributes?.['previousSession']).toBe('persisted-session');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does not load MMKV when persistence is disabled', () => {
|
|
113
|
+
initializeFaro(
|
|
114
|
+
mockConfig({
|
|
115
|
+
transports: [new MockTransport()],
|
|
116
|
+
instrumentations: [new SessionInstrumentation()],
|
|
117
|
+
sessionTracking: {
|
|
118
|
+
enabled: true,
|
|
119
|
+
persistent: false,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(mockCreateMMKV).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not link expired persisted state', () => {
|
|
128
|
+
const lastActivityAt = now - MAX_SESSION_PERSISTENCE_TIME;
|
|
129
|
+
mockMmkvValues.set(STORAGE_KEY, persistedSession({ startedAt: lastActivityAt - 10_000, lastActivityAt }));
|
|
130
|
+
|
|
131
|
+
const { faro } = initializePersistentSession();
|
|
132
|
+
|
|
133
|
+
expect(faro.metas.value.session?.id).toBe('new-session');
|
|
134
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
|
|
135
|
+
expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not link future-dated persisted state', () => {
|
|
139
|
+
mockMmkvValues.set(STORAGE_KEY, persistedSession({ startedAt: now + 1, lastActivityAt: now + 1 }));
|
|
140
|
+
|
|
141
|
+
const { faro } = initializePersistentSession();
|
|
142
|
+
|
|
143
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
|
|
144
|
+
expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not link a configured session ID to itself', () => {
|
|
148
|
+
mockMmkvValues.set(STORAGE_KEY, persistedSession({ currentSessionId: 'new-session' }));
|
|
149
|
+
|
|
150
|
+
const { faro } = initializePersistentSession();
|
|
151
|
+
|
|
152
|
+
expect(faro.metas.value.session?.id).toBe('new-session');
|
|
153
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it.each([
|
|
157
|
+
['corrupt', '{not-json'],
|
|
158
|
+
[
|
|
159
|
+
'unsupported',
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
schemaVersion: 99,
|
|
162
|
+
currentSessionId: 'persisted-session',
|
|
163
|
+
previousSessionId: null,
|
|
164
|
+
startedAt: now - 10_000,
|
|
165
|
+
lastActivityAt: now - 1,
|
|
166
|
+
isSampled: true,
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
])('starts cleanly when persisted state is %s', (_name, value) => {
|
|
170
|
+
mockMmkvValues.set(STORAGE_KEY, value);
|
|
171
|
+
|
|
172
|
+
const { faro } = initializePersistentSession();
|
|
173
|
+
|
|
174
|
+
expect(faro.metas.value.session?.id).toBe('new-session');
|
|
175
|
+
expect(faro.metas.value.session?.attributes?.['previousSession']).toBeUndefined();
|
|
176
|
+
expect(mockMmkv.remove).toHaveBeenCalledWith(STORAGE_KEY);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('stores only the new session and its link after initialization', () => {
|
|
180
|
+
mockMmkvValues.set(STORAGE_KEY, persistedSession());
|
|
181
|
+
|
|
182
|
+
initializePersistentSession();
|
|
183
|
+
|
|
184
|
+
const storedSession = mockMmkvValues.get(STORAGE_KEY);
|
|
185
|
+
expect(storedSession).toBeDefined();
|
|
186
|
+
expect(JSON.parse(storedSession ?? '')).toStrictEqual({
|
|
187
|
+
schemaVersion: 1,
|
|
188
|
+
currentSessionId: 'new-session',
|
|
189
|
+
previousSessionId: 'persisted-session',
|
|
190
|
+
startedAt: now,
|
|
191
|
+
lastActivityAt: now,
|
|
192
|
+
isSampled: true,
|
|
193
|
+
});
|
|
194
|
+
expect(MmkvPersistentSessionsManager.fetchUserSession()?.sessionId).toBe('new-session');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { EVENT_VIEW_CHANGED, TransportItemType } from '@grafana/faro-core';
|
|
2
|
+
import type { TransportItem } from '@grafana/faro-core';
|
|
3
|
+
|
|
4
|
+
import { EVENT_NAVIGATION } from '../../navigation/utils';
|
|
5
|
+
import { EVENT_APP_STATE_CHANGED } from '../appState';
|
|
6
|
+
|
|
7
|
+
import { classifySessionActivity, isRecoveredCrashItem, SessionActivityKind } from './sessionActivity';
|
|
8
|
+
|
|
9
|
+
function item(type: TransportItemType, payload: Record<string, unknown>, sessionId = 'current'): TransportItem {
|
|
10
|
+
return {
|
|
11
|
+
type,
|
|
12
|
+
payload: payload as TransportItem['payload'],
|
|
13
|
+
meta: { session: { id: sessionId } },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('session activity classification', () => {
|
|
18
|
+
it.each([
|
|
19
|
+
['view changes', item(TransportItemType.EVENT, { name: EVENT_VIEW_CHANGED })],
|
|
20
|
+
['navigation', item(TransportItemType.EVENT, { name: EVENT_NAVIGATION })],
|
|
21
|
+
[
|
|
22
|
+
'foreground returns',
|
|
23
|
+
item(TransportItemType.EVENT, {
|
|
24
|
+
name: EVENT_APP_STATE_CHANGED,
|
|
25
|
+
attributes: { fromState: 'background', toState: 'active' },
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
['tracked actions', item(TransportItemType.EXCEPTION, { action: { name: 'checkout' } })],
|
|
29
|
+
])('treats %s as meaningful', (_name, transportItem) => {
|
|
30
|
+
expect(classifySessionActivity(transportItem)).toBe(SessionActivityKind.Meaningful);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each([
|
|
34
|
+
['generic events', item(TransportItemType.EVENT, { name: 'poll_complete' })],
|
|
35
|
+
['unmarked HTTP events', item(TransportItemType.EVENT, { name: 'faro.tracing.fetch' })],
|
|
36
|
+
[
|
|
37
|
+
'background lifecycle events',
|
|
38
|
+
item(TransportItemType.EVENT, {
|
|
39
|
+
name: EVENT_APP_STATE_CHANGED,
|
|
40
|
+
attributes: { toState: 'background' },
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'returns from an inactive overlay',
|
|
45
|
+
item(TransportItemType.EVENT, {
|
|
46
|
+
name: EVENT_APP_STATE_CHANGED,
|
|
47
|
+
attributes: { fromState: 'inactive', toState: 'active' },
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
['logs', item(TransportItemType.LOG, { message: 'heartbeat' })],
|
|
51
|
+
['errors', item(TransportItemType.EXCEPTION, { type: 'error', value: 'failure' })],
|
|
52
|
+
['measurements', item(TransportItemType.MEASUREMENT, { type: 'cpu', values: { usage: 1 } })],
|
|
53
|
+
['traces', item(TransportItemType.TRACE, { trace_id: 'trace-id' })],
|
|
54
|
+
])('treats %s as passive', (_name, transportItem) => {
|
|
55
|
+
expect(classifySessionActivity(transportItem)).toBe(SessionActivityKind.Passive);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('recognizes a fatal crash recovered from a previous session', () => {
|
|
59
|
+
const recoveredCrash = item(
|
|
60
|
+
TransportItemType.EXCEPTION,
|
|
61
|
+
{ fatal: true, type: 'crash', value: 'native crash' },
|
|
62
|
+
'previous'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(isRecoveredCrashItem(recoveredCrash, 'current')).toBe(true);
|
|
66
|
+
expect(isRecoveredCrashItem(recoveredCrash, 'previous')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|