@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
package/src/navigation/utils.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { faro } from '@grafana/faro-core';
|
|
|
4
4
|
|
|
5
5
|
import { setCurrentScreen } from '../metas/screen';
|
|
6
6
|
|
|
7
|
+
export const EVENT_NAVIGATION = 'navigation';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Gets the currently active route from navigation state
|
|
9
11
|
*/
|
|
@@ -57,7 +59,7 @@ export function onNavigationStateChange(state: NavigationState | undefined): voi
|
|
|
57
59
|
|
|
58
60
|
// Optionally push additional attributes if route has params
|
|
59
61
|
if (currentRoute?.params) {
|
|
60
|
-
faro.api?.pushEvent(
|
|
62
|
+
faro.api?.pushEvent(EVENT_NAVIGATION, {
|
|
61
63
|
screen: screenName,
|
|
62
64
|
params: JSON.stringify(currentRoute.params),
|
|
63
65
|
});
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export { FetchTransport } from './transport';
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
FetchTransportOptions,
|
|
4
|
+
FetchTransportRequestOptions,
|
|
5
|
+
FetchTransportSendOutcome,
|
|
6
|
+
FetchTransportSendResult,
|
|
7
|
+
} from './types';
|
|
@@ -101,6 +101,81 @@ describe('FetchTransport', () => {
|
|
|
101
101
|
);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
it('uses the live session for the header while preserving the payload session', async () => {
|
|
105
|
+
const transport = new FetchTransport({
|
|
106
|
+
url: 'http://example.com/collect',
|
|
107
|
+
});
|
|
108
|
+
const waitForSession = jest
|
|
109
|
+
.spyOn(transport as unknown as { waitForSession: () => Promise<void> }, 'waitForSession')
|
|
110
|
+
.mockResolvedValue();
|
|
111
|
+
|
|
112
|
+
transport.metas.value = { session: { id: 'new-live-session' } };
|
|
113
|
+
transport.internalLogger = mockInternalLogger;
|
|
114
|
+
|
|
115
|
+
await transport.send([item]);
|
|
116
|
+
|
|
117
|
+
expect(waitForSession).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
119
|
+
'http://example.com/collect',
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
body: JSON.stringify(getTransportBody([item])),
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'x-faro-session-id': 'new-live-session',
|
|
125
|
+
},
|
|
126
|
+
method: 'POST',
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns whether the collector accepted the payload', async () => {
|
|
132
|
+
const transport = new FetchTransport({
|
|
133
|
+
url: 'http://example.com/collect',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
transport.metas.value = { session: { id: mockSessionId } };
|
|
137
|
+
transport.internalLogger = mockInternalLogger;
|
|
138
|
+
|
|
139
|
+
await expect(transport.sendWithResult([item])).resolves.toEqual({
|
|
140
|
+
outcome: 'accepted',
|
|
141
|
+
status: 202,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
fetch.mockImplementationOnce(() =>
|
|
145
|
+
Promise.resolve({
|
|
146
|
+
status: 500,
|
|
147
|
+
text: () => Promise.resolve('server error'),
|
|
148
|
+
headers: {
|
|
149
|
+
get: () => null,
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await expect(transport.sendWithResult([item])).resolves.toEqual({
|
|
155
|
+
outcome: 'rejected',
|
|
156
|
+
status: 500,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
fetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')));
|
|
160
|
+
|
|
161
|
+
await expect(transport.sendWithResult([item])).resolves.toEqual({
|
|
162
|
+
outcome: 'failed',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('skips empty batches without sending a request', async () => {
|
|
167
|
+
const transport = new FetchTransport({
|
|
168
|
+
url: 'http://example.com/collect',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
transport.internalLogger = mockInternalLogger;
|
|
172
|
+
|
|
173
|
+
await expect(transport.sendWithResult([])).resolves.toEqual({
|
|
174
|
+
outcome: 'skipped',
|
|
175
|
+
});
|
|
176
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
104
179
|
it('will send event with API key if provided', async () => {
|
|
105
180
|
const transport = new FetchTransport({
|
|
106
181
|
url: 'http://example.com/collect',
|
|
@@ -353,16 +428,25 @@ describe('FetchTransport', () => {
|
|
|
353
428
|
const transport = new FetchTransport({
|
|
354
429
|
url: 'http://example.com/collect',
|
|
355
430
|
});
|
|
431
|
+
const waitForSession = jest
|
|
432
|
+
.spyOn(transport as unknown as { waitForSession: () => Promise<void> }, 'waitForSession')
|
|
433
|
+
.mockResolvedValue();
|
|
356
434
|
|
|
357
435
|
transport.metas.value = {};
|
|
358
436
|
transport.internalLogger = mockInternalLogger;
|
|
359
437
|
|
|
360
|
-
|
|
438
|
+
const itemWithoutSession = {
|
|
439
|
+
...item,
|
|
440
|
+
meta: {},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
await transport.send([itemWithoutSession]);
|
|
361
444
|
|
|
445
|
+
expect(waitForSession).toHaveBeenCalledTimes(1);
|
|
362
446
|
expect(fetch).toHaveBeenCalledWith(
|
|
363
447
|
'http://example.com/collect',
|
|
364
448
|
expect.objectContaining({
|
|
365
|
-
body: JSON.stringify(getTransportBody([
|
|
449
|
+
body: JSON.stringify(getTransportBody([itemWithoutSession])),
|
|
366
450
|
headers: {
|
|
367
451
|
'Content-Type': 'application/json',
|
|
368
452
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseTransport, createPromiseBuffer, getTransportBody, VERSION } from '@grafana/faro-core';
|
|
2
2
|
import type { Patterns, PromiseBuffer, TransportItem } from '@grafana/faro-core';
|
|
3
3
|
|
|
4
|
-
import type { FetchTransportOptions } from './types';
|
|
4
|
+
import type { FetchTransportOptions, FetchTransportSendResult } from './types';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_BUFFER_SIZE = 30;
|
|
7
7
|
const DEFAULT_CONCURRENCY = 5;
|
|
@@ -10,8 +10,6 @@ const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
10
10
|
const FAILURE_BACKOFF_MS = 30000; // 30 seconds
|
|
11
11
|
|
|
12
12
|
const TOO_MANY_REQUESTS = 429;
|
|
13
|
-
const ACCEPTED = 202;
|
|
14
|
-
|
|
15
13
|
export class FetchTransport extends BaseTransport {
|
|
16
14
|
readonly name = '@grafana/faro-react-native:transport-fetch';
|
|
17
15
|
readonly version = VERSION;
|
|
@@ -111,8 +109,21 @@ export class FetchTransport extends BaseTransport {
|
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
async send(items: TransportItem[]): Promise<void> {
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
await this.sendWithResult(items);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sends a batch and reports whether the collector accepted it.
|
|
117
|
+
* The request header uses the live session for collector accounting. Payload
|
|
118
|
+
* metadata remains independent so delayed signals can retain their original
|
|
119
|
+
* session.
|
|
120
|
+
*/
|
|
121
|
+
async sendWithResult(items: TransportItem[]): Promise<FetchTransportSendResult> {
|
|
122
|
+
this.logDebug(`FetchTransport.sendWithResult() called with ${items.length} items`);
|
|
123
|
+
|
|
124
|
+
if (items.length === 0) {
|
|
125
|
+
return { outcome: 'skipped' };
|
|
126
|
+
}
|
|
116
127
|
|
|
117
128
|
try {
|
|
118
129
|
const now = new Date(this.getNow());
|
|
@@ -120,14 +131,15 @@ export class FetchTransport extends BaseTransport {
|
|
|
120
131
|
// Check if we're in backoff period
|
|
121
132
|
if (this.disabledUntil > now) {
|
|
122
133
|
this.logDebug(`FetchTransport: in backoff period until ${this.disabledUntil}`);
|
|
123
|
-
return
|
|
134
|
+
return { outcome: 'skipped' };
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
137
|
+
// The collector uses the header for session validation and accounting,
|
|
138
|
+
// independently of the session carried by each payload.
|
|
128
139
|
await this.waitForSession();
|
|
140
|
+
const sessionId = this.metas.value.session?.id;
|
|
129
141
|
|
|
130
|
-
await this.promiseBuffer.add(() => {
|
|
142
|
+
const response = await this.promiseBuffer.add(() => {
|
|
131
143
|
const transportBody = getTransportBody(items);
|
|
132
144
|
const body = JSON.stringify(transportBody);
|
|
133
145
|
|
|
@@ -142,12 +154,6 @@ export class FetchTransport extends BaseTransport {
|
|
|
142
154
|
|
|
143
155
|
const { headers, ...restOfRequestOptions } = requestOptions ?? {};
|
|
144
156
|
|
|
145
|
-
let sessionId;
|
|
146
|
-
const sessionMeta = this.metas.value.session;
|
|
147
|
-
if (sessionMeta != null) {
|
|
148
|
-
sessionId = sessionMeta.id;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
157
|
// DEBUG: Log fetch attempt
|
|
152
158
|
this.logDebug(`FetchTransport: sending ${items.length} items to ${url}`);
|
|
153
159
|
|
|
@@ -186,7 +192,7 @@ export class FetchTransport extends BaseTransport {
|
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
// Log non-success responses for debugging
|
|
189
|
-
if (response.status
|
|
195
|
+
if (response.status < 200 || response.status >= 300) {
|
|
190
196
|
const text = await response.text().catch(() => '');
|
|
191
197
|
this.logDebug(`FetchTransport: non-success response: ${response.status} ${text.slice(0, 200)}`);
|
|
192
198
|
}
|
|
@@ -211,11 +217,27 @@ export class FetchTransport extends BaseTransport {
|
|
|
211
217
|
|
|
212
218
|
// Do NOT log errors to console - this causes infinite loops in React Native
|
|
213
219
|
// when the DevTools console override intercepts even unpatchedConsole calls
|
|
220
|
+
return undefined;
|
|
214
221
|
});
|
|
215
222
|
});
|
|
223
|
+
|
|
224
|
+
if (!response) {
|
|
225
|
+
return { outcome: 'failed' };
|
|
226
|
+
}
|
|
227
|
+
if (response.status >= 200 && response.status < 300) {
|
|
228
|
+
return {
|
|
229
|
+
outcome: 'accepted',
|
|
230
|
+
status: response.status,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
outcome: 'rejected',
|
|
235
|
+
status: response.status,
|
|
236
|
+
};
|
|
216
237
|
} catch {
|
|
217
238
|
// Buffer full error - Do NOT log to console as it creates infinite loops
|
|
218
239
|
// The error is typically "Task buffer full" when the device is offline
|
|
240
|
+
return { outcome: 'failed' };
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
243
|
|
|
@@ -23,3 +23,10 @@ export interface FetchTransportOptions {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export type ClockFn = () => number;
|
|
26
|
+
|
|
27
|
+
export type FetchTransportSendOutcome = 'accepted' | 'rejected' | 'failed' | 'skipped';
|
|
28
|
+
|
|
29
|
+
export interface FetchTransportSendResult {
|
|
30
|
+
outcome: FetchTransportSendOutcome;
|
|
31
|
+
status?: number;
|
|
32
|
+
}
|
|
@@ -23,7 +23,7 @@ const DEFAULT_CHECK_INTERVAL_MS = 30000;
|
|
|
23
23
|
* Uses @react-native-community/netinfo when available, otherwise falls back
|
|
24
24
|
* to basic fetch-based connectivity detection.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* Monitors network connectivity for offline transport flush behavior.
|
|
27
27
|
*/
|
|
28
28
|
export class DefaultConnectivityService implements ConnectivityService {
|
|
29
29
|
private _isOnline: boolean = true;
|
|
@@ -16,7 +16,7 @@ const DEFAULT_MAX_CACHE_SIZE = 1000;
|
|
|
16
16
|
* File-based offline cache implementation using AsyncStorage.
|
|
17
17
|
*
|
|
18
18
|
* Stores cached telemetry payloads as JSONL (one JSON object per line)
|
|
19
|
-
*
|
|
19
|
+
* Persistent offline telemetry cache backed by AsyncStorage.
|
|
20
20
|
*
|
|
21
21
|
* Uses a mutex pattern for thread-safe file access.
|
|
22
22
|
*/
|
|
@@ -77,7 +77,7 @@ export class AsyncStorageOfflineCache implements OfflineCache {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Execute an operation with a mutex lock to ensure thread-safe access.
|
|
80
|
-
*
|
|
80
|
+
* Async lock to serialize cache read/write operations.
|
|
81
81
|
*/
|
|
82
82
|
private async withLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
83
83
|
const currentLock = this.lockPromise;
|
|
@@ -17,8 +17,7 @@ const DEFAULT_MAX_CACHE_DURATION_MS = 3 * 24 * 60 * 60 * 1000;
|
|
|
17
17
|
* When the device is offline, telemetry is cached to AsyncStorage.
|
|
18
18
|
* When connectivity is restored, cached telemetry is replayed through the wrapped transports.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* - Uses AsyncStorage for persistent caching (matching Flutter's SharedPreferences)
|
|
20
|
+
* Uses AsyncStorage for persistent offline telemetry caching.
|
|
22
21
|
* - Respects maxCacheDuration to skip expired entries
|
|
23
22
|
* - Uses mutex pattern for thread-safe cache access
|
|
24
23
|
* - Excludes itself when replaying to prevent infinite loops
|
|
@@ -157,7 +156,7 @@ export class OfflineTransport extends BaseTransport {
|
|
|
157
156
|
|
|
158
157
|
/**
|
|
159
158
|
* Replay cached payloads through other transports.
|
|
160
|
-
*
|
|
159
|
+
* Reads cached telemetry from AsyncStorage.
|
|
161
160
|
*/
|
|
162
161
|
private async replayCachedPayloads(): Promise<void> {
|
|
163
162
|
if (this.isReplaying) {
|
|
@@ -207,7 +206,7 @@ export class OfflineTransport extends BaseTransport {
|
|
|
207
206
|
|
|
208
207
|
/**
|
|
209
208
|
* Send items to all other registered transports.
|
|
210
|
-
*
|
|
209
|
+
* Sends cached telemetry when connectivity is restored.
|
|
211
210
|
*/
|
|
212
211
|
private async sendToOtherTransports(items: TransportItem[]): Promise<boolean> {
|
|
213
212
|
if (this.otherTransports.length === 0) {
|
|
@@ -15,8 +15,7 @@ const DEFAULT_STORAGE_KEY = 'faro_persisted_user';
|
|
|
15
15
|
* Stores user information persistently so it can be restored on app restart.
|
|
16
16
|
* This ensures early telemetry events include user identification.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* - Uses AsyncStorage for persistence (matching Flutter's SharedPreferences)
|
|
18
|
+
* Uses AsyncStorage for persistent user data across app sessions.
|
|
20
19
|
* - Stores user as JSON
|
|
21
20
|
* - Handles null/cleared users
|
|
22
21
|
*
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.throttle = throttle;
|
|
4
|
-
/**
|
|
5
|
-
* Creates a throttled function that only invokes the provided function at most once per
|
|
6
|
-
* every `wait` milliseconds.
|
|
7
|
-
*
|
|
8
|
-
* @param func - The function to throttle
|
|
9
|
-
* @param wait - The number of milliseconds to throttle invocations to
|
|
10
|
-
* @returns A throttled version of the function
|
|
11
|
-
*/
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility needs to accept any function signature
|
|
13
|
-
function throttle(func, wait) {
|
|
14
|
-
var timeout = null;
|
|
15
|
-
var previous = 0;
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need to preserve function context
|
|
17
|
-
return function () {
|
|
18
|
-
var _this = this;
|
|
19
|
-
var args = [];
|
|
20
|
-
for (var _i = 0; _i < arguments.length; _i++) {
|
|
21
|
-
args[_i] = arguments[_i];
|
|
22
|
-
}
|
|
23
|
-
var now = Date.now();
|
|
24
|
-
var remaining = wait - (now - previous);
|
|
25
|
-
if (remaining <= 0 || remaining > wait) {
|
|
26
|
-
if (timeout) {
|
|
27
|
-
clearTimeout(timeout);
|
|
28
|
-
timeout = null;
|
|
29
|
-
}
|
|
30
|
-
previous = now;
|
|
31
|
-
func.apply(this, args);
|
|
32
|
-
}
|
|
33
|
-
else if (!timeout) {
|
|
34
|
-
timeout = setTimeout(function () {
|
|
35
|
-
previous = Date.now();
|
|
36
|
-
timeout = null;
|
|
37
|
-
func.apply(_this, args);
|
|
38
|
-
}, remaining);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
//# sourceMappingURL=throttle.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"throttle.js","sourceRoot":"","sources":["../../../src/utils/throttle.ts"],"names":[],"mappings":";;AASA,4BAwBC;AAjCD;;;;;;;GAOG;AACH,wHAAwH;AACxH,SAAgB,QAAQ,CAAoC,IAAO,EAAE,IAAY;IAC/E,IAAI,OAAO,GAAyC,IAAI,CAAC;IACzD,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,mGAAmG;IACnG,OAAO;QAAA,iBAkBN;QAlB2B,cAAsB;aAAtB,UAAsB,EAAtB,qBAAsB,EAAtB,IAAsB;YAAtB,yBAAsB;;QAChD,IAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC;QAE1C,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS,GAAG,IAAI,EAAE,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC;gBACZ,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,QAAQ,GAAG,GAAG,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,GAAG,UAAU,CAAC;gBACnB,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;gBACf,IAAI,CAAC,KAAK,CAAC,KAAI,EAAE,IAAI,CAAC,CAAC;YACzB,CAAC,EAAE,SAAS,CAAC,CAAC;QAChB,CAAC;IACH,CAAM,CAAC;AACT,CAAC","sourcesContent":["/**\n * Creates a throttled function that only invokes the provided function at most once per\n * every `wait` milliseconds.\n *\n * @param func - The function to throttle\n * @param wait - The number of milliseconds to throttle invocations to\n * @returns A throttled version of the function\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility needs to accept any function signature\nexport function throttle<T extends (...args: any[]) => any>(func: T, wait: number): T {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let previous = 0;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need to preserve function context\n return function (this: any, ...args: Parameters<T>) {\n const now = Date.now();\n const remaining = wait - (now - previous);\n\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n func.apply(this, args);\n } else if (!timeout) {\n timeout = setTimeout(() => {\n previous = Date.now();\n timeout = null;\n func.apply(this, args);\n }, remaining);\n }\n } as T;\n}\n"]}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a throttled function that only invokes the provided function at most once per
|
|
3
|
-
* every `wait` milliseconds.
|
|
4
|
-
*
|
|
5
|
-
* @param func - The function to throttle
|
|
6
|
-
* @param wait - The number of milliseconds to throttle invocations to
|
|
7
|
-
* @returns A throttled version of the function
|
|
8
|
-
*/
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility needs to accept any function signature
|
|
10
|
-
export function throttle(func, wait) {
|
|
11
|
-
let timeout = null;
|
|
12
|
-
let previous = 0;
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need to preserve function context
|
|
14
|
-
return function (...args) {
|
|
15
|
-
const now = Date.now();
|
|
16
|
-
const remaining = wait - (now - previous);
|
|
17
|
-
if (remaining <= 0 || remaining > wait) {
|
|
18
|
-
if (timeout) {
|
|
19
|
-
clearTimeout(timeout);
|
|
20
|
-
timeout = null;
|
|
21
|
-
}
|
|
22
|
-
previous = now;
|
|
23
|
-
func.apply(this, args);
|
|
24
|
-
}
|
|
25
|
-
else if (!timeout) {
|
|
26
|
-
timeout = setTimeout(() => {
|
|
27
|
-
previous = Date.now();
|
|
28
|
-
timeout = null;
|
|
29
|
-
func.apply(this, args);
|
|
30
|
-
}, remaining);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
//# sourceMappingURL=throttle.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"throttle.js","sourceRoot":"","sources":["../../../src/utils/throttle.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wHAAwH;AACxH,MAAM,UAAU,QAAQ,CAAoC,IAAO,EAAE,IAAY;IAC/E,IAAI,OAAO,GAAyC,IAAI,CAAC;IACzD,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,mGAAmG;IACnG,OAAO,UAAqB,GAAG,IAAmB;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC;QAE1C,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS,GAAG,IAAI,EAAE,CAAC;YACvC,IAAI,OAAO,EAAE,CAAC;gBACZ,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,QAAQ,GAAG,GAAG,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzB,CAAC;aAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBACxB,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACtB,OAAO,GAAG,IAAI,CAAC;gBACf,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACzB,CAAC,EAAE,SAAS,CAAC,CAAC;QAChB,CAAC;IACH,CAAM,CAAC;AACT,CAAC","sourcesContent":["/**\n * Creates a throttled function that only invokes the provided function at most once per\n * every `wait` milliseconds.\n *\n * @param func - The function to throttle\n * @param wait - The number of milliseconds to throttle invocations to\n * @returns A throttled version of the function\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility needs to accept any function signature\nexport function throttle<T extends (...args: any[]) => any>(func: T, wait: number): T {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n let previous = 0;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need to preserve function context\n return function (this: any, ...args: Parameters<T>) {\n const now = Date.now();\n const remaining = wait - (now - previous);\n\n if (remaining <= 0 || remaining > wait) {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n previous = now;\n func.apply(this, args);\n } else if (!timeout) {\n timeout = setTimeout(() => {\n previous = Date.now();\n timeout = null;\n func.apply(this, args);\n }, remaining);\n }\n } as T;\n}\n"]}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a throttled function that only invokes the provided function at most once per
|
|
3
|
-
* every `wait` milliseconds.
|
|
4
|
-
*
|
|
5
|
-
* @param func - The function to throttle
|
|
6
|
-
* @param wait - The number of milliseconds to throttle invocations to
|
|
7
|
-
* @returns A throttled version of the function
|
|
8
|
-
*/
|
|
9
|
-
export declare function throttle<T extends (...args: any[]) => any>(func: T, wait: number): T;
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { throttle } from './throttle';
|
|
2
|
-
|
|
3
|
-
describe('throttle', () => {
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
jest.useFakeTimers();
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
jest.useRealTimers();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should call the function immediately on first invocation', () => {
|
|
13
|
-
const func = jest.fn();
|
|
14
|
-
const throttled = throttle(func, 1000);
|
|
15
|
-
|
|
16
|
-
throttled();
|
|
17
|
-
|
|
18
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should throttle subsequent calls within the wait period', () => {
|
|
22
|
-
const func = jest.fn();
|
|
23
|
-
const throttled = throttle(func, 1000);
|
|
24
|
-
|
|
25
|
-
throttled();
|
|
26
|
-
throttled();
|
|
27
|
-
throttled();
|
|
28
|
-
|
|
29
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should allow a call after the wait period has elapsed', () => {
|
|
33
|
-
const func = jest.fn();
|
|
34
|
-
const throttled = throttle(func, 1000);
|
|
35
|
-
|
|
36
|
-
throttled();
|
|
37
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
38
|
-
|
|
39
|
-
jest.advanceTimersByTime(1000);
|
|
40
|
-
|
|
41
|
-
throttled();
|
|
42
|
-
expect(func).toHaveBeenCalledTimes(2);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should schedule a trailing call if called during throttle period', () => {
|
|
46
|
-
const func = jest.fn();
|
|
47
|
-
const throttled = throttle(func, 1000);
|
|
48
|
-
|
|
49
|
-
throttled('first');
|
|
50
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
51
|
-
expect(func).toHaveBeenCalledWith('first');
|
|
52
|
-
|
|
53
|
-
// Call again during throttle period
|
|
54
|
-
throttled('second');
|
|
55
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
56
|
-
|
|
57
|
-
// Advance timers to trigger the trailing call
|
|
58
|
-
jest.advanceTimersByTime(1000);
|
|
59
|
-
expect(func).toHaveBeenCalledTimes(2);
|
|
60
|
-
expect(func).toHaveBeenLastCalledWith('second');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should pass arguments correctly', () => {
|
|
64
|
-
const func = jest.fn();
|
|
65
|
-
const throttled = throttle(func, 1000);
|
|
66
|
-
|
|
67
|
-
throttled(1, 'test', { foo: 'bar' });
|
|
68
|
-
|
|
69
|
-
expect(func).toHaveBeenCalledWith(1, 'test', { foo: 'bar' });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should preserve context (this)', () => {
|
|
73
|
-
const obj = {
|
|
74
|
-
value: 42,
|
|
75
|
-
method: jest.fn(function (this: any) {
|
|
76
|
-
return this.value;
|
|
77
|
-
}),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const throttled = throttle(obj.method, 1000);
|
|
81
|
-
throttled.call(obj);
|
|
82
|
-
|
|
83
|
-
expect(obj.method).toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should only keep the last call during throttle period', () => {
|
|
87
|
-
const func = jest.fn();
|
|
88
|
-
const throttled = throttle(func, 1000);
|
|
89
|
-
|
|
90
|
-
throttled('call1');
|
|
91
|
-
throttled('call2');
|
|
92
|
-
throttled('call3');
|
|
93
|
-
|
|
94
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
95
|
-
expect(func).toHaveBeenCalledWith('call1');
|
|
96
|
-
|
|
97
|
-
// Advance time to trigger trailing call
|
|
98
|
-
// The throttle keeps only the first scheduled call, not the last
|
|
99
|
-
jest.advanceTimersByTime(1000);
|
|
100
|
-
|
|
101
|
-
expect(func).toHaveBeenCalledTimes(2);
|
|
102
|
-
// The trailing call uses the arguments from when timeout was set (call2)
|
|
103
|
-
expect(func).toHaveBeenLastCalledWith('call2');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should handle rapid successive calls correctly', () => {
|
|
107
|
-
const func = jest.fn();
|
|
108
|
-
const throttled = throttle(func, 100);
|
|
109
|
-
|
|
110
|
-
// First call - executes immediately
|
|
111
|
-
throttled(1);
|
|
112
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
113
|
-
|
|
114
|
-
// Calls during throttle - schedules timeout with first call's args
|
|
115
|
-
throttled(2);
|
|
116
|
-
throttled(3);
|
|
117
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
118
|
-
|
|
119
|
-
// Advance to trigger trailing call
|
|
120
|
-
jest.advanceTimersByTime(100);
|
|
121
|
-
expect(func).toHaveBeenCalledTimes(2);
|
|
122
|
-
// Uses args from when timeout was first set (2)
|
|
123
|
-
expect(func).toHaveBeenLastCalledWith(2);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should execute trailing call after throttle period', () => {
|
|
127
|
-
const func = jest.fn();
|
|
128
|
-
const throttled = throttle(func, 1000);
|
|
129
|
-
|
|
130
|
-
throttled('first');
|
|
131
|
-
expect(func).toHaveBeenCalledTimes(1);
|
|
132
|
-
|
|
133
|
-
// Call during throttle period - schedules trailing call
|
|
134
|
-
throttled('second');
|
|
135
|
-
|
|
136
|
-
// Advance past the wait period
|
|
137
|
-
jest.advanceTimersByTime(1000);
|
|
138
|
-
expect(func).toHaveBeenCalledTimes(2);
|
|
139
|
-
expect(func).toHaveBeenLastCalledWith('second');
|
|
140
|
-
});
|
|
141
|
-
});
|
package/src/utils/throttle.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a throttled function that only invokes the provided function at most once per
|
|
3
|
-
* every `wait` milliseconds.
|
|
4
|
-
*
|
|
5
|
-
* @param func - The function to throttle
|
|
6
|
-
* @param wait - The number of milliseconds to throttle invocations to
|
|
7
|
-
* @returns A throttled version of the function
|
|
8
|
-
*/
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic utility needs to accept any function signature
|
|
10
|
-
export function throttle<T extends (...args: any[]) => any>(func: T, wait: number): T {
|
|
11
|
-
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
12
|
-
let previous = 0;
|
|
13
|
-
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Need to preserve function context
|
|
15
|
-
return function (this: any, ...args: Parameters<T>) {
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const remaining = wait - (now - previous);
|
|
18
|
-
|
|
19
|
-
if (remaining <= 0 || remaining > wait) {
|
|
20
|
-
if (timeout) {
|
|
21
|
-
clearTimeout(timeout);
|
|
22
|
-
timeout = null;
|
|
23
|
-
}
|
|
24
|
-
previous = now;
|
|
25
|
-
func.apply(this, args);
|
|
26
|
-
} else if (!timeout) {
|
|
27
|
-
timeout = setTimeout(() => {
|
|
28
|
-
previous = Date.now();
|
|
29
|
-
timeout = null;
|
|
30
|
-
func.apply(this, args);
|
|
31
|
-
}, remaining);
|
|
32
|
-
}
|
|
33
|
-
} as T;
|
|
34
|
-
}
|