@formo/analytics-react-native 0.1.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 (247) hide show
  1. package/README.md +302 -0
  2. package/lib/commonjs/FormoAnalytics.js +526 -0
  3. package/lib/commonjs/FormoAnalytics.js.map +1 -0
  4. package/lib/commonjs/FormoAnalyticsProvider.js +265 -0
  5. package/lib/commonjs/FormoAnalyticsProvider.js.map +1 -0
  6. package/lib/commonjs/constants/config.js +69 -0
  7. package/lib/commonjs/constants/config.js.map +1 -0
  8. package/lib/commonjs/constants/events.js +30 -0
  9. package/lib/commonjs/constants/events.js.map +1 -0
  10. package/lib/commonjs/constants/index.js +39 -0
  11. package/lib/commonjs/constants/index.js.map +1 -0
  12. package/lib/commonjs/constants/storage.js +23 -0
  13. package/lib/commonjs/constants/storage.js.map +1 -0
  14. package/lib/commonjs/index.js +65 -0
  15. package/lib/commonjs/index.js.map +1 -0
  16. package/lib/commonjs/lib/consent/index.js +56 -0
  17. package/lib/commonjs/lib/consent/index.js.map +1 -0
  18. package/lib/commonjs/lib/event/EventFactory.js +493 -0
  19. package/lib/commonjs/lib/event/EventFactory.js.map +1 -0
  20. package/lib/commonjs/lib/event/EventManager.js +46 -0
  21. package/lib/commonjs/lib/event/EventManager.js.map +1 -0
  22. package/lib/commonjs/lib/event/EventQueue.js +290 -0
  23. package/lib/commonjs/lib/event/EventQueue.js.map +1 -0
  24. package/lib/commonjs/lib/event/index.js +50 -0
  25. package/lib/commonjs/lib/event/index.js.map +1 -0
  26. package/lib/commonjs/lib/event/types.js +6 -0
  27. package/lib/commonjs/lib/event/types.js.map +1 -0
  28. package/lib/commonjs/lib/lifecycle/index.js +196 -0
  29. package/lib/commonjs/lib/lifecycle/index.js.map +1 -0
  30. package/lib/commonjs/lib/logger/index.js +48 -0
  31. package/lib/commonjs/lib/logger/index.js.map +1 -0
  32. package/lib/commonjs/lib/session/index.js +109 -0
  33. package/lib/commonjs/lib/session/index.js.map +1 -0
  34. package/lib/commonjs/lib/storage/AsyncStorageAdapter.js +164 -0
  35. package/lib/commonjs/lib/storage/AsyncStorageAdapter.js.map +1 -0
  36. package/lib/commonjs/lib/storage/MemoryStorage.js +41 -0
  37. package/lib/commonjs/lib/storage/MemoryStorage.js.map +1 -0
  38. package/lib/commonjs/lib/storage/StorageBlueprint.js +24 -0
  39. package/lib/commonjs/lib/storage/StorageBlueprint.js.map +1 -0
  40. package/lib/commonjs/lib/storage/StorageManager.js +126 -0
  41. package/lib/commonjs/lib/storage/StorageManager.js.map +1 -0
  42. package/lib/commonjs/lib/storage/index.js +49 -0
  43. package/lib/commonjs/lib/storage/index.js.map +1 -0
  44. package/lib/commonjs/lib/storage/types.js +2 -0
  45. package/lib/commonjs/lib/storage/types.js.map +1 -0
  46. package/lib/commonjs/lib/wagmi/WagmiEventHandler.js +445 -0
  47. package/lib/commonjs/lib/wagmi/WagmiEventHandler.js.map +1 -0
  48. package/lib/commonjs/lib/wagmi/index.js +28 -0
  49. package/lib/commonjs/lib/wagmi/index.js.map +1 -0
  50. package/lib/commonjs/lib/wagmi/types.js +2 -0
  51. package/lib/commonjs/lib/wagmi/types.js.map +1 -0
  52. package/lib/commonjs/types/base.js +6 -0
  53. package/lib/commonjs/types/base.js.map +1 -0
  54. package/lib/commonjs/types/events.js +22 -0
  55. package/lib/commonjs/types/events.js.map +1 -0
  56. package/lib/commonjs/types/index.js +28 -0
  57. package/lib/commonjs/types/index.js.map +1 -0
  58. package/lib/commonjs/utils/address.js +82 -0
  59. package/lib/commonjs/utils/address.js.map +1 -0
  60. package/lib/commonjs/utils/hash.js +30 -0
  61. package/lib/commonjs/utils/hash.js.map +1 -0
  62. package/lib/commonjs/utils/helpers.js +116 -0
  63. package/lib/commonjs/utils/helpers.js.map +1 -0
  64. package/lib/commonjs/utils/index.js +61 -0
  65. package/lib/commonjs/utils/index.js.map +1 -0
  66. package/lib/commonjs/utils/timestamp.js +34 -0
  67. package/lib/commonjs/utils/timestamp.js.map +1 -0
  68. package/lib/commonjs/utils/trafficSource.js +147 -0
  69. package/lib/commonjs/utils/trafficSource.js.map +1 -0
  70. package/lib/commonjs/version.js +10 -0
  71. package/lib/commonjs/version.js.map +1 -0
  72. package/lib/module/FormoAnalytics.js +519 -0
  73. package/lib/module/FormoAnalytics.js.map +1 -0
  74. package/lib/module/FormoAnalyticsProvider.js +256 -0
  75. package/lib/module/FormoAnalyticsProvider.js.map +1 -0
  76. package/lib/module/constants/config.js +62 -0
  77. package/lib/module/constants/config.js.map +1 -0
  78. package/lib/module/constants/events.js +24 -0
  79. package/lib/module/constants/events.js.map +1 -0
  80. package/lib/module/constants/index.js +4 -0
  81. package/lib/module/constants/index.js.map +1 -0
  82. package/lib/module/constants/storage.js +17 -0
  83. package/lib/module/constants/storage.js.map +1 -0
  84. package/lib/module/index.js +51 -0
  85. package/lib/module/index.js.map +1 -0
  86. package/lib/module/lib/consent/index.js +49 -0
  87. package/lib/module/lib/consent/index.js.map +1 -0
  88. package/lib/module/lib/event/EventFactory.js +488 -0
  89. package/lib/module/lib/event/EventFactory.js.map +1 -0
  90. package/lib/module/lib/event/EventManager.js +41 -0
  91. package/lib/module/lib/event/EventManager.js.map +1 -0
  92. package/lib/module/lib/event/EventQueue.js +283 -0
  93. package/lib/module/lib/event/EventQueue.js.map +1 -0
  94. package/lib/module/lib/event/index.js +5 -0
  95. package/lib/module/lib/event/index.js.map +1 -0
  96. package/lib/module/lib/event/types.js +2 -0
  97. package/lib/module/lib/event/types.js.map +1 -0
  98. package/lib/module/lib/lifecycle/index.js +190 -0
  99. package/lib/module/lib/lifecycle/index.js.map +1 -0
  100. package/lib/module/lib/logger/index.js +42 -0
  101. package/lib/module/lib/logger/index.js.map +1 -0
  102. package/lib/module/lib/session/index.js +92 -0
  103. package/lib/module/lib/session/index.js.map +1 -0
  104. package/lib/module/lib/storage/AsyncStorageAdapter.js +158 -0
  105. package/lib/module/lib/storage/AsyncStorageAdapter.js.map +1 -0
  106. package/lib/module/lib/storage/MemoryStorage.js +35 -0
  107. package/lib/module/lib/storage/MemoryStorage.js.map +1 -0
  108. package/lib/module/lib/storage/StorageBlueprint.js +18 -0
  109. package/lib/module/lib/storage/StorageBlueprint.js.map +1 -0
  110. package/lib/module/lib/storage/StorageManager.js +115 -0
  111. package/lib/module/lib/storage/StorageManager.js.map +1 -0
  112. package/lib/module/lib/storage/index.js +5 -0
  113. package/lib/module/lib/storage/index.js.map +1 -0
  114. package/lib/module/lib/storage/types.js +2 -0
  115. package/lib/module/lib/storage/types.js.map +1 -0
  116. package/lib/module/lib/wagmi/WagmiEventHandler.js +439 -0
  117. package/lib/module/lib/wagmi/WagmiEventHandler.js.map +1 -0
  118. package/lib/module/lib/wagmi/index.js +3 -0
  119. package/lib/module/lib/wagmi/index.js.map +1 -0
  120. package/lib/module/lib/wagmi/types.js +2 -0
  121. package/lib/module/lib/wagmi/types.js.map +1 -0
  122. package/lib/module/types/base.js +2 -0
  123. package/lib/module/types/base.js.map +1 -0
  124. package/lib/module/types/events.js +17 -0
  125. package/lib/module/types/events.js.map +1 -0
  126. package/lib/module/types/index.js +3 -0
  127. package/lib/module/types/index.js.map +1 -0
  128. package/lib/module/utils/address.js +74 -0
  129. package/lib/module/utils/address.js.map +1 -0
  130. package/lib/module/utils/hash.js +24 -0
  131. package/lib/module/utils/hash.js.map +1 -0
  132. package/lib/module/utils/helpers.js +105 -0
  133. package/lib/module/utils/helpers.js.map +1 -0
  134. package/lib/module/utils/index.js +6 -0
  135. package/lib/module/utils/index.js.map +1 -0
  136. package/lib/module/utils/timestamp.js +26 -0
  137. package/lib/module/utils/timestamp.js.map +1 -0
  138. package/lib/module/utils/trafficSource.js +137 -0
  139. package/lib/module/utils/trafficSource.js.map +1 -0
  140. package/lib/module/version.js +4 -0
  141. package/lib/module/version.js.map +1 -0
  142. package/lib/typescript/FormoAnalytics.d.ts +163 -0
  143. package/lib/typescript/FormoAnalytics.d.ts.map +1 -0
  144. package/lib/typescript/FormoAnalyticsProvider.d.ts +29 -0
  145. package/lib/typescript/FormoAnalyticsProvider.d.ts.map +1 -0
  146. package/lib/typescript/constants/config.d.ts +8 -0
  147. package/lib/typescript/constants/config.d.ts.map +1 -0
  148. package/lib/typescript/constants/events.d.ts +23 -0
  149. package/lib/typescript/constants/events.d.ts.map +1 -0
  150. package/lib/typescript/constants/index.d.ts +4 -0
  151. package/lib/typescript/constants/index.d.ts.map +1 -0
  152. package/lib/typescript/constants/storage.d.ts +10 -0
  153. package/lib/typescript/constants/storage.d.ts.map +1 -0
  154. package/lib/typescript/index.d.ts +44 -0
  155. package/lib/typescript/index.d.ts.map +1 -0
  156. package/lib/typescript/lib/consent/index.d.ts +13 -0
  157. package/lib/typescript/lib/consent/index.d.ts.map +1 -0
  158. package/lib/typescript/lib/event/EventFactory.d.ts +61 -0
  159. package/lib/typescript/lib/event/EventFactory.d.ts.map +1 -0
  160. package/lib/typescript/lib/event/EventManager.d.ts +17 -0
  161. package/lib/typescript/lib/event/EventManager.d.ts.map +1 -0
  162. package/lib/typescript/lib/event/EventQueue.d.ts +74 -0
  163. package/lib/typescript/lib/event/EventQueue.d.ts.map +1 -0
  164. package/lib/typescript/lib/event/index.d.ts +5 -0
  165. package/lib/typescript/lib/event/index.d.ts.map +1 -0
  166. package/lib/typescript/lib/event/types.d.ts +23 -0
  167. package/lib/typescript/lib/event/types.d.ts.map +1 -0
  168. package/lib/typescript/lib/lifecycle/index.d.ts +46 -0
  169. package/lib/typescript/lib/lifecycle/index.d.ts.map +1 -0
  170. package/lib/typescript/lib/logger/index.d.ts +19 -0
  171. package/lib/typescript/lib/logger/index.d.ts.map +1 -0
  172. package/lib/typescript/lib/session/index.d.ts +41 -0
  173. package/lib/typescript/lib/session/index.d.ts.map +1 -0
  174. package/lib/typescript/lib/storage/AsyncStorageAdapter.d.ts +48 -0
  175. package/lib/typescript/lib/storage/AsyncStorageAdapter.d.ts.map +1 -0
  176. package/lib/typescript/lib/storage/MemoryStorage.d.ts +18 -0
  177. package/lib/typescript/lib/storage/MemoryStorage.d.ts.map +1 -0
  178. package/lib/typescript/lib/storage/StorageBlueprint.d.ts +21 -0
  179. package/lib/typescript/lib/storage/StorageBlueprint.d.ts.map +1 -0
  180. package/lib/typescript/lib/storage/StorageManager.d.ts +45 -0
  181. package/lib/typescript/lib/storage/StorageManager.d.ts.map +1 -0
  182. package/lib/typescript/lib/storage/index.d.ts +5 -0
  183. package/lib/typescript/lib/storage/index.d.ts.map +1 -0
  184. package/lib/typescript/lib/storage/types.d.ts +22 -0
  185. package/lib/typescript/lib/storage/types.d.ts.map +1 -0
  186. package/lib/typescript/lib/wagmi/WagmiEventHandler.d.ts +104 -0
  187. package/lib/typescript/lib/wagmi/WagmiEventHandler.d.ts.map +1 -0
  188. package/lib/typescript/lib/wagmi/index.d.ts +3 -0
  189. package/lib/typescript/lib/wagmi/index.d.ts.map +1 -0
  190. package/lib/typescript/lib/wagmi/types.d.ts +54 -0
  191. package/lib/typescript/lib/wagmi/types.d.ts.map +1 -0
  192. package/lib/typescript/types/base.d.ts +219 -0
  193. package/lib/typescript/types/base.d.ts.map +1 -0
  194. package/lib/typescript/types/events.d.ts +111 -0
  195. package/lib/typescript/types/events.d.ts.map +1 -0
  196. package/lib/typescript/types/index.d.ts +3 -0
  197. package/lib/typescript/types/index.d.ts.map +1 -0
  198. package/lib/typescript/utils/address.d.ts +25 -0
  199. package/lib/typescript/utils/address.d.ts.map +1 -0
  200. package/lib/typescript/utils/hash.d.ts +10 -0
  201. package/lib/typescript/utils/hash.d.ts.map +1 -0
  202. package/lib/typescript/utils/helpers.d.ts +26 -0
  203. package/lib/typescript/utils/helpers.d.ts.map +1 -0
  204. package/lib/typescript/utils/index.d.ts +6 -0
  205. package/lib/typescript/utils/index.d.ts.map +1 -0
  206. package/lib/typescript/utils/timestamp.d.ts +13 -0
  207. package/lib/typescript/utils/timestamp.d.ts.map +1 -0
  208. package/lib/typescript/utils/trafficSource.d.ts +30 -0
  209. package/lib/typescript/utils/trafficSource.d.ts.map +1 -0
  210. package/lib/typescript/version.d.ts +2 -0
  211. package/lib/typescript/version.d.ts.map +1 -0
  212. package/package.json +143 -0
  213. package/src/FormoAnalytics.ts +685 -0
  214. package/src/FormoAnalyticsProvider.tsx +296 -0
  215. package/src/constants/config.ts +62 -0
  216. package/src/constants/events.ts +26 -0
  217. package/src/constants/index.ts +3 -0
  218. package/src/constants/storage.ts +16 -0
  219. package/src/index.ts +55 -0
  220. package/src/lib/consent/index.ts +52 -0
  221. package/src/lib/event/EventFactory.ts +682 -0
  222. package/src/lib/event/EventManager.ts +50 -0
  223. package/src/lib/event/EventQueue.ts +371 -0
  224. package/src/lib/event/index.ts +4 -0
  225. package/src/lib/event/types.ts +107 -0
  226. package/src/lib/lifecycle/index.ts +215 -0
  227. package/src/lib/logger/index.ts +56 -0
  228. package/src/lib/session/index.ts +103 -0
  229. package/src/lib/storage/AsyncStorageAdapter.ts +173 -0
  230. package/src/lib/storage/MemoryStorage.ts +43 -0
  231. package/src/lib/storage/StorageBlueprint.ts +30 -0
  232. package/src/lib/storage/StorageManager.ts +121 -0
  233. package/src/lib/storage/index.ts +4 -0
  234. package/src/lib/storage/types.ts +23 -0
  235. package/src/lib/wagmi/WagmiEventHandler.ts +574 -0
  236. package/src/lib/wagmi/index.ts +2 -0
  237. package/src/lib/wagmi/types.ts +71 -0
  238. package/src/types/base.ts +287 -0
  239. package/src/types/events.ts +140 -0
  240. package/src/types/index.ts +2 -0
  241. package/src/utils/address.ts +84 -0
  242. package/src/utils/hash.ts +23 -0
  243. package/src/utils/helpers.ts +139 -0
  244. package/src/utils/index.ts +5 -0
  245. package/src/utils/timestamp.ts +25 -0
  246. package/src/utils/trafficSource.ts +153 -0
  247. package/src/version.ts +3 -0
@@ -0,0 +1,50 @@
1
+ import { Address, APIEvent, Options } from "../../types";
2
+ import { logger } from "../logger";
3
+ import { EventFactory } from "./EventFactory";
4
+ import { IEventFactory, IEventManager, IEventQueue } from "./types";
5
+ import { isBlockedAddress } from "../../utils/address";
6
+
7
+ /**
8
+ * Event manager for React Native SDK
9
+ * Generates valid event payloads and queues them for processing
10
+ */
11
+ class EventManager implements IEventManager {
12
+ eventQueue: IEventQueue;
13
+ eventFactory: IEventFactory;
14
+
15
+ constructor(eventQueue: IEventQueue, options?: Options) {
16
+ this.eventQueue = eventQueue;
17
+ this.eventFactory = new EventFactory(options);
18
+ }
19
+
20
+ /**
21
+ * Add event to queue
22
+ */
23
+ async addEvent(
24
+ event: APIEvent,
25
+ address?: Address,
26
+ userId?: string
27
+ ): Promise<void> {
28
+ const { callback, ..._event } = event;
29
+ const formoEvent = await this.eventFactory.create(_event, address, userId);
30
+
31
+ // Check if the final event has a blocked address
32
+ if (formoEvent.address && isBlockedAddress(formoEvent.address)) {
33
+ logger.warn(
34
+ `Event blocked: Address ${formoEvent.address} is in the blocked list`
35
+ );
36
+ return;
37
+ }
38
+
39
+ await this.eventQueue.enqueue(formoEvent, (err, _, data) => {
40
+ if (err) {
41
+ logger.error("Error sending events:", err);
42
+ } else {
43
+ logger.info(`Events sent successfully: ${(data as unknown[])?.length ?? 0} events`);
44
+ }
45
+ callback?.(err, _, data);
46
+ });
47
+ }
48
+ }
49
+
50
+ export { EventManager };
@@ -0,0 +1,371 @@
1
+ import { AppState, AppStateStatus } from "react-native";
2
+ import { IFormoEvent, IFormoEventPayload } from "../../types";
3
+ import { EVENTS_API_REQUEST_HEADER } from "../../constants";
4
+ import {
5
+ clampNumber,
6
+ getActionDescriptor,
7
+ millisecondsToSecond,
8
+ isNetworkError,
9
+ } from "../../utils";
10
+ import { hash } from "../../utils/hash";
11
+ import { toDateHourMinute } from "../../utils/timestamp";
12
+ import { logger } from "../logger";
13
+ import { IEventQueue } from "./types";
14
+
15
+ type QueueItem = {
16
+ message: IFormoEventPayload;
17
+ callback: (...args: unknown[]) => void;
18
+ hash: string;
19
+ };
20
+
21
+ type IFormoEventFlushPayload = IFormoEventPayload & {
22
+ sent_at: string;
23
+ };
24
+
25
+ interface Options {
26
+ apiHost: string;
27
+ flushAt?: number;
28
+ flushInterval?: number;
29
+ retryCount?: number;
30
+ maxQueueSize?: number;
31
+ }
32
+
33
+ const DEFAULT_RETRY = 3;
34
+ const MAX_RETRY = 5;
35
+ const MIN_RETRY = 1;
36
+
37
+ const DEFAULT_FLUSH_AT = 20;
38
+ const MAX_FLUSH_AT = 20;
39
+ const MIN_FLUSH_AT = 1;
40
+
41
+ const DEFAULT_QUEUE_SIZE = 1_024 * 500; // 500kB
42
+ const MAX_QUEUE_SIZE = 1_024 * 500; // 500kB
43
+ const MIN_QUEUE_SIZE = 200; // 200 bytes
44
+
45
+ const DEFAULT_FLUSH_INTERVAL = 1_000 * 30; // 30 seconds
46
+ const MAX_FLUSH_INTERVAL = 1_000 * 300; // 5 minutes
47
+ const MIN_FLUSH_INTERVAL = 1_000 * 10; // 10 seconds
48
+
49
+ const noop = () => {};
50
+
51
+ /**
52
+ * Event queue for React Native
53
+ * Handles batching, flushing, and retries with app lifecycle awareness
54
+ */
55
+ export class EventQueue implements IEventQueue {
56
+ private writeKey: string;
57
+ private apiHost: string;
58
+ private queue: QueueItem[] = [];
59
+ private timer: ReturnType<typeof setTimeout> | null = null;
60
+ private flushAt: number;
61
+ private flushIntervalMs: number;
62
+ private maxQueueSize: number;
63
+ private retryCount: number;
64
+ private payloadHashes: Set<string> = new Set();
65
+ private flushMutex: Promise<void> = Promise.resolve();
66
+ private appStateSubscription: { remove: () => void } | null = null;
67
+
68
+ constructor(writeKey: string, options: Options) {
69
+ this.writeKey = writeKey;
70
+ this.apiHost = options.apiHost;
71
+ this.retryCount = clampNumber(
72
+ options.retryCount || DEFAULT_RETRY,
73
+ MAX_RETRY,
74
+ MIN_RETRY
75
+ );
76
+ this.flushAt = clampNumber(
77
+ options.flushAt || DEFAULT_FLUSH_AT,
78
+ MAX_FLUSH_AT,
79
+ MIN_FLUSH_AT
80
+ );
81
+ this.maxQueueSize = clampNumber(
82
+ options.maxQueueSize || DEFAULT_QUEUE_SIZE,
83
+ MAX_QUEUE_SIZE,
84
+ MIN_QUEUE_SIZE
85
+ );
86
+ this.flushIntervalMs = clampNumber(
87
+ options.flushInterval || DEFAULT_FLUSH_INTERVAL,
88
+ MAX_FLUSH_INTERVAL,
89
+ MIN_FLUSH_INTERVAL
90
+ );
91
+ // Set up app state listener for React Native
92
+ this.setupAppStateListener();
93
+ }
94
+
95
+ /**
96
+ * Set up listener for app state changes
97
+ * Flush events when app goes to background
98
+ */
99
+ private setupAppStateListener(): void {
100
+ this.appStateSubscription = AppState.addEventListener(
101
+ "change",
102
+ this.handleAppStateChange.bind(this)
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Handle app state changes
108
+ */
109
+ private handleAppStateChange(nextAppState: AppStateStatus): void {
110
+ // Flush when app goes to background or becomes inactive
111
+ if (nextAppState === "background" || nextAppState === "inactive") {
112
+ logger.debug("EventQueue: App going to background, flushing events");
113
+ this.flush().catch((error) => {
114
+ logger.error("EventQueue: Failed to flush on background", error);
115
+ });
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate message ID for deduplication
121
+ */
122
+ private async generateMessageId(event: IFormoEvent): Promise<string> {
123
+ const formattedTimestamp = toDateHourMinute(
124
+ new Date(event.original_timestamp)
125
+ );
126
+ const eventForHashing = { ...event, original_timestamp: formattedTimestamp };
127
+ const eventString = JSON.stringify(eventForHashing);
128
+ return hash(eventString);
129
+ }
130
+
131
+ /**
132
+ * Check if event is a duplicate
133
+ */
134
+ private isDuplicate(eventId: string): boolean {
135
+ if (this.payloadHashes.has(eventId)) return true;
136
+ this.payloadHashes.add(eventId);
137
+ return false;
138
+ }
139
+
140
+ /**
141
+ * Add event to queue
142
+ */
143
+ async enqueue(
144
+ event: IFormoEvent,
145
+ callback?: (...args: unknown[]) => void
146
+ ): Promise<void> {
147
+ callback = callback || noop;
148
+
149
+ const message_id = await this.generateMessageId(event);
150
+
151
+ // Check for duplicate
152
+ if (this.isDuplicate(message_id)) {
153
+ logger.warn(
154
+ `Event already enqueued, try again after ${millisecondsToSecond(
155
+ this.flushIntervalMs
156
+ )} seconds.`
157
+ );
158
+ return;
159
+ }
160
+
161
+ this.queue.push({
162
+ message: { ...event, message_id },
163
+ callback,
164
+ hash: message_id,
165
+ });
166
+
167
+ logger.log(
168
+ `Event enqueued: ${getActionDescriptor(event.type, event.properties)}`
169
+ );
170
+
171
+ const hasReachedFlushAt = this.queue.length >= this.flushAt;
172
+ const hasReachedQueueSize =
173
+ this.queue.reduce(
174
+ (acc, item) => acc + JSON.stringify(item).length,
175
+ 0
176
+ ) >= this.maxQueueSize;
177
+
178
+ if (hasReachedFlushAt || hasReachedQueueSize) {
179
+ // Clear timer to prevent double flush
180
+ if (this.timer) {
181
+ clearTimeout(this.timer);
182
+ this.timer = null;
183
+ }
184
+ // Flush uses internal mutex to serialize operations
185
+ this.flush().catch((error) => {
186
+ logger.error("EventQueue: Failed to flush on threshold", error);
187
+ });
188
+ return;
189
+ }
190
+
191
+ if (this.flushIntervalMs && !this.timer) {
192
+ this.timer = setTimeout(this.flush.bind(this), this.flushIntervalMs);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Flush events to API
198
+ * Uses a mutex to ensure only one flush operation runs at a time,
199
+ * preventing race conditions with re-queued items on failure.
200
+ */
201
+ async flush(callback?: (...args: unknown[]) => void): Promise<void> {
202
+ callback = callback || noop;
203
+
204
+ if (this.timer) {
205
+ clearTimeout(this.timer);
206
+ this.timer = null;
207
+ }
208
+
209
+ // Use mutex to serialize flush operations and prevent race conditions
210
+ const previousMutex = this.flushMutex;
211
+ let resolveMutex: () => void;
212
+ this.flushMutex = new Promise((resolve) => {
213
+ resolveMutex = resolve;
214
+ });
215
+
216
+ try {
217
+ // Wait for any previous flush to complete
218
+ await previousMutex;
219
+
220
+ if (!this.queue.length) {
221
+ callback();
222
+ return;
223
+ }
224
+
225
+ const items = this.queue.splice(0, this.flushAt);
226
+
227
+ const sentAt = new Date().toISOString();
228
+ const data: IFormoEventFlushPayload[] = items.map((item) => ({
229
+ ...item.message,
230
+ sent_at: sentAt,
231
+ }));
232
+
233
+ const done = (err?: Error) => {
234
+ items.forEach(({ message, callback: itemCallback }) =>
235
+ itemCallback(err, message, data)
236
+ );
237
+ callback!(err, data);
238
+ };
239
+
240
+ try {
241
+ await this.sendWithRetry(data);
242
+ // Only remove hashes after successful send
243
+ items.forEach((item) => this.payloadHashes.delete(item.hash));
244
+ done();
245
+ logger.info(`Events sent successfully: ${data.length} events`);
246
+ } catch (err) {
247
+ // Re-add items to the front of the queue for retry on next flush
248
+ // Note: We intentionally keep hashes in payloadHashes to prevent duplicate
249
+ // events from being enqueued while these items are pending retry.
250
+ this.queue.unshift(...items);
251
+ done(err as Error);
252
+ logger.error("Error sending events, re-queued for retry:", err);
253
+ throw err;
254
+ }
255
+ } finally {
256
+ resolveMutex!();
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Send events with retry logic
262
+ */
263
+ private async sendWithRetry(
264
+ data: IFormoEventFlushPayload[],
265
+ attempt = 0
266
+ ): Promise<void> {
267
+ try {
268
+ const response = await fetch(this.apiHost, {
269
+ method: "POST",
270
+ headers: EVENTS_API_REQUEST_HEADER(this.writeKey),
271
+ body: JSON.stringify(data),
272
+ });
273
+
274
+ if (!response.ok) {
275
+ const shouldRetry = this.shouldRetry(response.status);
276
+ if (shouldRetry && attempt < this.retryCount) {
277
+ const delay = Math.pow(2, attempt) * 1000;
278
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), delay));
279
+ return this.sendWithRetry(data, attempt + 1);
280
+ }
281
+ throw new Error(`HTTP error! status: ${response.status}`);
282
+ }
283
+ } catch (error) {
284
+ if (isNetworkError(error) && attempt < this.retryCount) {
285
+ const delay = Math.pow(2, attempt) * 1000;
286
+ logger.warn(`Network error, retrying in ${delay}ms...`);
287
+ await new Promise<void>((resolve) => setTimeout(() => resolve(), delay));
288
+ return this.sendWithRetry(data, attempt + 1);
289
+ }
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Check if error should be retried
296
+ */
297
+ private shouldRetry(status: number): boolean {
298
+ // Retry on server errors (5xx) and rate limiting (429)
299
+ return (status >= 500 && status <= 599) || status === 429;
300
+ }
301
+
302
+ /**
303
+ * Discard all pending events without sending them.
304
+ * Used when the user opts out of tracking to prevent queued events
305
+ * from being sent after consent is revoked.
306
+ */
307
+ public clear(): void {
308
+ this.queue = [];
309
+ this.payloadHashes.clear();
310
+
311
+ if (this.timer) {
312
+ clearTimeout(this.timer);
313
+ this.timer = null;
314
+ }
315
+
316
+ logger.debug("EventQueue: Cleared all pending events");
317
+ }
318
+
319
+ /**
320
+ * Clean up resources, flushing any pending events first
321
+ */
322
+ public async cleanup(): Promise<void> {
323
+ // Flush all remaining queued events before teardown
324
+ // Loop until queue is empty since flush() only sends flushAt events per call
325
+ // Safety limit prevents infinite loops if flush silently fails
326
+ const maxAttempts = Math.ceil(this.queue.length / this.flushAt) + 3;
327
+ let attempts = 0;
328
+ const initialQueueLength = this.queue.length;
329
+
330
+ while (this.queue.length > 0 && attempts < maxAttempts) {
331
+ const queueLengthBefore = this.queue.length;
332
+ try {
333
+ await this.flush();
334
+ } catch (error) {
335
+ logger.error("EventQueue: Failed to flush during cleanup", error);
336
+ // Break on error to avoid infinite loop if flush keeps failing
337
+ break;
338
+ }
339
+
340
+ // If queue length didn't decrease, flush is silently failing
341
+ if (this.queue.length >= queueLengthBefore) {
342
+ logger.warn("EventQueue: Flush did not reduce queue size, aborting cleanup");
343
+ break;
344
+ }
345
+
346
+ attempts++;
347
+ }
348
+
349
+ if (attempts >= maxAttempts && this.queue.length > 0) {
350
+ logger.warn(
351
+ `EventQueue: Cleanup safety limit reached. Discarding ${this.queue.length} events.`
352
+ );
353
+ this.queue = [];
354
+ this.payloadHashes.clear();
355
+ }
356
+
357
+ if (initialQueueLength > 0) {
358
+ logger.debug(`EventQueue: Cleanup completed, flushed ${initialQueueLength - this.queue.length} events`);
359
+ }
360
+
361
+ if (this.timer) {
362
+ clearTimeout(this.timer);
363
+ this.timer = null;
364
+ }
365
+
366
+ if (this.appStateSubscription) {
367
+ this.appStateSubscription.remove();
368
+ this.appStateSubscription = null;
369
+ }
370
+ }
371
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./EventFactory";
2
+ export * from "./EventManager";
3
+ export * from "./EventQueue";
4
+ export * from "./types";
@@ -0,0 +1,107 @@
1
+ import {
2
+ Address,
3
+ APIEvent,
4
+ IFormoEvent,
5
+ IFormoEventContext,
6
+ IFormoEventProperties,
7
+ Nullable,
8
+ SignatureStatus,
9
+ TransactionStatus,
10
+ ChainID,
11
+ } from "../../types";
12
+
13
+ export interface IEventFactory {
14
+ create(
15
+ event: APIEvent,
16
+ address?: Address,
17
+ userId?: string
18
+ ): Promise<IFormoEvent>;
19
+
20
+ generateScreenEvent(
21
+ name: string,
22
+ category?: string,
23
+ properties?: IFormoEventProperties,
24
+ context?: IFormoEventContext
25
+ ): Promise<IFormoEvent>;
26
+
27
+ generateDetectWalletEvent(
28
+ providerName: string,
29
+ rdns: string,
30
+ properties?: IFormoEventProperties,
31
+ context?: IFormoEventContext
32
+ ): Promise<IFormoEvent>;
33
+
34
+ generateIdentifyEvent(
35
+ providerName: string,
36
+ rdns: string,
37
+ address: Nullable<Address>,
38
+ userId?: Nullable<string>,
39
+ properties?: IFormoEventProperties,
40
+ context?: IFormoEventContext
41
+ ): Promise<IFormoEvent>;
42
+
43
+ generateConnectEvent(
44
+ chainId: ChainID,
45
+ address: Address,
46
+ properties?: IFormoEventProperties,
47
+ context?: IFormoEventContext
48
+ ): Promise<IFormoEvent>;
49
+
50
+ generateDisconnectEvent(
51
+ chainId?: ChainID,
52
+ address?: Address,
53
+ properties?: IFormoEventProperties,
54
+ context?: IFormoEventContext
55
+ ): Promise<IFormoEvent>;
56
+
57
+ generateChainChangedEvent(
58
+ chainId: ChainID,
59
+ address: Address,
60
+ properties?: IFormoEventProperties,
61
+ context?: IFormoEventContext
62
+ ): Promise<IFormoEvent>;
63
+
64
+ generateSignatureEvent(
65
+ status: SignatureStatus,
66
+ chainId: ChainID | undefined,
67
+ address: Address,
68
+ message: string,
69
+ signatureHash?: string,
70
+ properties?: IFormoEventProperties,
71
+ context?: IFormoEventContext
72
+ ): Promise<IFormoEvent>;
73
+
74
+ generateTransactionEvent(
75
+ status: TransactionStatus,
76
+ chainId: ChainID,
77
+ address: Address,
78
+ data?: string,
79
+ to?: string,
80
+ value?: string,
81
+ transactionHash?: string,
82
+ function_name?: string,
83
+ function_args?: Record<string, unknown>,
84
+ properties?: IFormoEventProperties,
85
+ context?: IFormoEventContext
86
+ ): Promise<IFormoEvent>;
87
+
88
+ generateTrackEvent(
89
+ event: string,
90
+ properties?: IFormoEventProperties,
91
+ context?: IFormoEventContext
92
+ ): Promise<IFormoEvent>;
93
+ }
94
+
95
+ export interface IEventManager {
96
+ addEvent(event: APIEvent, address?: Address, userId?: string): Promise<void>;
97
+ }
98
+
99
+ export interface IEventQueue {
100
+ enqueue(
101
+ event: IFormoEvent,
102
+ callback?: (...args: unknown[]) => void
103
+ ): Promise<void>;
104
+ flush(callback?: (...args: unknown[]) => void): Promise<void>;
105
+ clear(): void;
106
+ cleanup(): Promise<void>;
107
+ }