@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.
Files changed (266) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +20 -15
  3. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
  4. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
  5. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
  9. package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
  10. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
  11. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
  12. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
  13. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
  14. package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
  15. package/dist/cjs/config/getRNInstrumentations.js +6 -8
  16. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  17. package/dist/cjs/config/sampling.js.map +1 -1
  18. package/dist/cjs/config/types.js.map +1 -1
  19. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  20. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  21. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  22. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  23. package/dist/cjs/index.js +8 -3
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  26. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  27. package/dist/cjs/instrumentations/appState/index.js +8 -2
  28. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  29. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  30. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  32. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  33. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  34. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  35. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/errors/index.js +8 -3
  39. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  41. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  43. package/dist/cjs/instrumentations/performance/index.js +2 -6
  44. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  45. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  46. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  47. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  48. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/index.js +214 -45
  50. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  51. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  52. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  53. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  54. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  55. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  56. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  58. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  60. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  61. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  62. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  66. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  67. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  68. package/dist/cjs/instrumentations/startup/index.js +2 -6
  69. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  70. package/dist/cjs/navigation/utils.js +3 -1
  71. package/dist/cjs/navigation/utils.js.map +1 -1
  72. package/dist/cjs/transports/fetch/index.js.map +1 -1
  73. package/dist/cjs/transports/fetch/transport.js +56 -25
  74. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  75. package/dist/cjs/transports/fetch/types.js.map +1 -1
  76. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  77. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  78. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  79. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  80. package/dist/cjs/transports/offline/transport.js +3 -4
  81. package/dist/cjs/transports/offline/transport.js.map +1 -1
  82. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  83. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  84. package/dist/esm/config/getRNInstrumentations.js +6 -8
  85. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  86. package/dist/esm/config/sampling.js.map +1 -1
  87. package/dist/esm/config/types.js.map +1 -1
  88. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  89. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  90. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  91. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  92. package/dist/esm/index.js +4 -2
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  95. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  96. package/dist/esm/instrumentations/appState/index.js +8 -2
  97. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  98. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  99. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  101. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  102. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  103. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  105. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  106. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  107. package/dist/esm/instrumentations/errors/index.js +8 -3
  108. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  109. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  110. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  111. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  112. package/dist/esm/instrumentations/performance/index.js +2 -6
  113. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  114. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  115. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  116. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  117. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  118. package/dist/esm/instrumentations/session/index.js +207 -46
  119. package/dist/esm/instrumentations/session/index.js.map +1 -1
  120. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  121. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  123. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  125. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  126. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  127. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  129. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  130. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  131. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  133. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  134. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  135. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  136. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  137. package/dist/esm/instrumentations/startup/index.js +2 -6
  138. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  139. package/dist/esm/navigation/utils.js +2 -1
  140. package/dist/esm/navigation/utils.js.map +1 -1
  141. package/dist/esm/transports/fetch/index.js.map +1 -1
  142. package/dist/esm/transports/fetch/transport.js +38 -14
  143. package/dist/esm/transports/fetch/transport.js.map +1 -1
  144. package/dist/esm/transports/fetch/types.js.map +1 -1
  145. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  146. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  147. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  148. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  149. package/dist/esm/transports/offline/transport.js +3 -4
  150. package/dist/esm/transports/offline/transport.js.map +1 -1
  151. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  152. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  153. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  154. package/dist/types/config/sampling.d.ts +0 -3
  155. package/dist/types/config/types.d.ts +5 -3
  156. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  157. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  158. package/dist/types/index.d.ts +4 -1
  159. package/dist/types/instrumentations/anr/instrumentation.d.ts +1 -1
  160. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  161. package/dist/types/instrumentations/console/index.d.ts +1 -1
  162. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  163. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
  164. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  165. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
  166. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
  167. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  168. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
  169. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  170. package/dist/types/instrumentations/http/index.d.ts +1 -1
  171. package/dist/types/instrumentations/performance/index.d.ts +1 -3
  172. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  173. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  174. package/dist/types/instrumentations/session/index.d.ts +21 -2
  175. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  176. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  177. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  178. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  179. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  180. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  181. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  182. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  183. package/dist/types/instrumentations/startup/index.d.ts +3 -7
  184. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  185. package/dist/types/instrumentations/view/index.d.ts +1 -1
  186. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  187. package/dist/types/navigation/utils.d.ts +1 -0
  188. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  189. package/dist/types/transports/console/transport.d.ts +1 -1
  190. package/dist/types/transports/fetch/index.d.ts +1 -1
  191. package/dist/types/transports/fetch/transport.d.ts +9 -2
  192. package/dist/types/transports/fetch/types.d.ts +5 -0
  193. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  194. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  195. package/dist/types/transports/offline/transport.d.ts +4 -5
  196. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  197. package/ios/FaroCrashReporter.swift +214 -21
  198. package/ios/FaroReactNative.swift +24 -0
  199. package/ios/FaroReactNativeModule.h +2 -1
  200. package/ios/FaroReactNativeModule.mm +121 -6
  201. package/ios/RefreshRateVitals.swift +34 -38
  202. package/package.json +8 -6
  203. package/src/config/getRNInstrumentations.ts +6 -8
  204. package/src/config/makeRNConfig.test.ts +6 -0
  205. package/src/config/sampling.ts +0 -3
  206. package/src/config/types.ts +5 -3
  207. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  208. package/src/generated/faroRNPackageMeta.ts +1 -1
  209. package/src/index.ts +8 -2
  210. package/src/instrumentations/anr/instrumentation.ts +8 -1
  211. package/src/instrumentations/appState/index.ts +10 -2
  212. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  213. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  214. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  215. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  216. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  217. package/src/instrumentations/crashReporting/types.ts +7 -0
  218. package/src/instrumentations/errors/index.ts +12 -4
  219. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  220. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  221. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  222. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  223. package/src/instrumentations/performance/index.ts +2 -6
  224. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  225. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  226. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  227. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  228. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  229. package/src/instrumentations/session/index.ts +269 -46
  230. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  231. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  232. package/src/instrumentations/session/sessionActivity.ts +58 -0
  233. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  234. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  235. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  236. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  237. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  238. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  239. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  240. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  241. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  242. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  243. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  244. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  245. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  246. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  247. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  248. package/src/instrumentations/session/sessionProcess.ts +114 -0
  249. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  250. package/src/instrumentations/startup/index.ts +2 -6
  251. package/src/navigation/utils.ts +3 -1
  252. package/src/transports/fetch/index.ts +6 -1
  253. package/src/transports/fetch/transport.test.ts +86 -2
  254. package/src/transports/fetch/transport.ts +38 -16
  255. package/src/transports/fetch/types.ts +7 -0
  256. package/src/transports/offline/ConnectivityService.ts +1 -1
  257. package/src/transports/offline/OfflineCache.ts +2 -2
  258. package/src/transports/offline/transport.ts +3 -4
  259. package/src/userPersistence/UserPersistence.ts +1 -2
  260. package/dist/cjs/utils/throttle.js +0 -42
  261. package/dist/cjs/utils/throttle.js.map +0 -1
  262. package/dist/esm/utils/throttle.js +0 -34
  263. package/dist/esm/utils/throttle.js.map +0 -1
  264. package/dist/types/utils/throttle.d.ts +0 -9
  265. package/src/utils/throttle.test.ts +0 -141
  266. package/src/utils/throttle.ts +0 -34
@@ -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('navigation', {
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 { FetchTransportOptions, FetchTransportRequestOptions } from './types';
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
- await transport.send([item]);
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([item])),
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
- // DEBUG: Log at the very start of send
115
- this.logDebug(`FetchTransport.send() called with ${items.length} items`);
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 Promise.resolve();
134
+ return { outcome: 'skipped' };
124
135
  }
125
136
 
126
- // Wait for session to be ready before sending
127
- // This prevents 400 errors from missing X-Faro-Session-Id header
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 !== ACCEPTED && response.status !== 200) {
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
- * Implementation follows Flutter SDK's InternetConnectivityService pattern.
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
- * following the Flutter SDK's offline transport pattern.
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
- * Follows Flutter SDK's Completer-based lock pattern.
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
- * Implementation follows Flutter SDK's OfflineTransport pattern:
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
- * Follows Flutter SDK's _readFromFile pattern.
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
- * Follows Flutter SDK's _sendCachedData pattern.
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
- * Implementation follows Flutter SDK's UserPersistence pattern:
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
- });
@@ -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
- }