@formo/analytics 1.13.4 → 1.14.2

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 (124) hide show
  1. package/.github/workflows/ci.yml +51 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/dist/cjs/src/FormoAnalytics.d.ts +6 -5
  4. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  5. package/dist/cjs/src/FormoAnalytics.js +88 -83
  6. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  7. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +4 -5
  8. package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -1
  9. package/dist/cjs/src/FormoAnalyticsProvider.js +39 -62
  10. package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -1
  11. package/dist/cjs/src/constants/base.d.ts +3 -0
  12. package/dist/cjs/src/constants/base.d.ts.map +1 -0
  13. package/dist/cjs/src/constants/base.js +6 -0
  14. package/dist/cjs/src/constants/base.js.map +1 -0
  15. package/dist/cjs/src/constants/config.d.ts +1 -1
  16. package/dist/cjs/src/constants/config.js +58 -58
  17. package/dist/cjs/src/constants/config.js.map +1 -1
  18. package/dist/cjs/src/constants/index.d.ts +4 -2
  19. package/dist/cjs/src/constants/index.d.ts.map +1 -1
  20. package/dist/cjs/src/constants/index.js +2 -0
  21. package/dist/cjs/src/constants/index.js.map +1 -1
  22. package/dist/cjs/src/constants/regex.d.ts +4 -0
  23. package/dist/cjs/src/constants/regex.d.ts.map +1 -0
  24. package/dist/cjs/src/constants/regex.js +7 -0
  25. package/dist/cjs/src/constants/regex.js.map +1 -0
  26. package/dist/cjs/src/lib/fingerprint.d.ts +4 -0
  27. package/dist/cjs/src/lib/fingerprint.d.ts.map +1 -0
  28. package/dist/cjs/src/lib/fingerprint.js +63 -0
  29. package/dist/cjs/src/lib/fingerprint.js.map +1 -0
  30. package/dist/cjs/src/lib/index.d.ts +3 -0
  31. package/dist/cjs/src/lib/index.d.ts.map +1 -0
  32. package/dist/cjs/src/lib/index.js +24 -0
  33. package/dist/cjs/src/lib/index.js.map +1 -0
  34. package/dist/cjs/src/lib/queue.d.ts +33 -0
  35. package/dist/cjs/src/lib/queue.d.ts.map +1 -0
  36. package/dist/cjs/src/lib/queue.js +319 -0
  37. package/dist/cjs/src/lib/queue.js.map +1 -0
  38. package/dist/cjs/src/lib/session-storage.d.ts +11 -0
  39. package/dist/cjs/src/lib/session-storage.d.ts.map +1 -0
  40. package/dist/cjs/src/lib/session-storage.js +52 -0
  41. package/dist/cjs/src/lib/session-storage.js.map +1 -0
  42. package/dist/cjs/src/lib/utils.d.ts +6 -0
  43. package/dist/cjs/src/lib/utils.d.ts.map +1 -1
  44. package/dist/cjs/src/lib/utils.js +32 -0
  45. package/dist/cjs/src/lib/utils.js.map +1 -1
  46. package/dist/cjs/src/types/base.d.ts +8 -2
  47. package/dist/cjs/src/types/base.d.ts.map +1 -1
  48. package/dist/cjs/src/types/events.d.ts +7 -0
  49. package/dist/cjs/src/types/events.d.ts.map +1 -1
  50. package/dist/cjs/src/types/events.js.map +1 -1
  51. package/dist/cjs/test/lib.spec.js +11 -3
  52. package/dist/cjs/test/lib.spec.js.map +1 -1
  53. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  54. package/dist/esm/src/FormoAnalytics.d.ts +6 -5
  55. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  56. package/dist/esm/src/FormoAnalytics.js +87 -79
  57. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  58. package/dist/esm/src/FormoAnalyticsProvider.d.ts +4 -5
  59. package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -1
  60. package/dist/esm/src/FormoAnalyticsProvider.js +40 -63
  61. package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -1
  62. package/dist/esm/src/constants/base.d.ts +3 -0
  63. package/dist/esm/src/constants/base.d.ts.map +1 -0
  64. package/dist/esm/src/constants/base.js +3 -0
  65. package/dist/esm/src/constants/base.js.map +1 -0
  66. package/dist/esm/src/constants/config.d.ts +1 -1
  67. package/dist/esm/src/constants/config.js +58 -58
  68. package/dist/esm/src/constants/config.js.map +1 -1
  69. package/dist/esm/src/constants/index.d.ts +4 -2
  70. package/dist/esm/src/constants/index.d.ts.map +1 -1
  71. package/dist/esm/src/constants/index.js +4 -2
  72. package/dist/esm/src/constants/index.js.map +1 -1
  73. package/dist/esm/src/constants/regex.d.ts +4 -0
  74. package/dist/esm/src/constants/regex.d.ts.map +1 -0
  75. package/dist/esm/src/constants/regex.js +4 -0
  76. package/dist/esm/src/constants/regex.js.map +1 -0
  77. package/dist/esm/src/lib/fingerprint.d.ts +4 -0
  78. package/dist/esm/src/lib/fingerprint.d.ts.map +1 -0
  79. package/dist/esm/src/lib/fingerprint.js +60 -0
  80. package/dist/esm/src/lib/fingerprint.js.map +1 -0
  81. package/dist/esm/src/lib/index.d.ts +3 -0
  82. package/dist/esm/src/lib/index.d.ts.map +1 -0
  83. package/dist/esm/src/lib/index.js +3 -0
  84. package/dist/esm/src/lib/index.js.map +1 -0
  85. package/dist/esm/src/lib/queue.d.ts +33 -0
  86. package/dist/esm/src/lib/queue.d.ts.map +1 -0
  87. package/dist/esm/src/lib/queue.js +313 -0
  88. package/dist/esm/src/lib/queue.js.map +1 -0
  89. package/dist/esm/src/lib/session-storage.d.ts +11 -0
  90. package/dist/esm/src/lib/session-storage.d.ts.map +1 -0
  91. package/dist/esm/src/lib/session-storage.js +49 -0
  92. package/dist/esm/src/lib/session-storage.js.map +1 -0
  93. package/dist/esm/src/lib/utils.d.ts +6 -0
  94. package/dist/esm/src/lib/utils.d.ts.map +1 -1
  95. package/dist/esm/src/lib/utils.js +25 -0
  96. package/dist/esm/src/lib/utils.js.map +1 -1
  97. package/dist/esm/src/types/base.d.ts +8 -2
  98. package/dist/esm/src/types/base.d.ts.map +1 -1
  99. package/dist/esm/src/types/events.d.ts +7 -0
  100. package/dist/esm/src/types/events.d.ts.map +1 -1
  101. package/dist/esm/src/types/events.js.map +1 -1
  102. package/dist/esm/test/lib.spec.js +9 -1
  103. package/dist/esm/test/lib.spec.js.map +1 -1
  104. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  105. package/dist/index.umd.min.js +1 -1
  106. package/dist/index.umd.min.js.LICENSE.txt +0 -12
  107. package/dist/index.umd.min.js.map +1 -1
  108. package/package.json +7 -6
  109. package/src/FormoAnalytics.ts +86 -66
  110. package/src/FormoAnalyticsProvider.tsx +32 -67
  111. package/src/constants/base.ts +2 -0
  112. package/src/constants/config.ts +58 -58
  113. package/src/constants/index.ts +4 -2
  114. package/src/constants/regex.ts +3 -0
  115. package/src/lib/fingerprint.ts +9 -0
  116. package/src/lib/index.ts +2 -0
  117. package/src/lib/queue.ts +310 -0
  118. package/src/lib/session-storage.ts +53 -0
  119. package/src/lib/utils.ts +31 -0
  120. package/src/types/base.ts +12 -5
  121. package/src/types/events.ts +15 -7
  122. package/test/lib.spec.ts +13 -1
  123. package/dist/387.index.umd.min.js +0 -125
  124. package/dist/387.index.umd.min.js.map +0 -1
@@ -64,7 +64,7 @@ export const COUNTRY_LIST = {
64
64
  "America/Araguaina": "BR",
65
65
  "America/Argentina/Buenos_Aires": "AR",
66
66
  "America/Argentina/Catamarca": "AR",
67
- "America/Argentina/ComodRivadavi": "AR",
67
+ "America/Argentina/ComodRivadavia": "AR",
68
68
  "America/Argentina/Cordoba": "AR",
69
69
  "America/Argentina/Jujuy": "AR",
70
70
  "America/Argentina/La_Rioja": "AR",
@@ -181,7 +181,7 @@ export const COUNTRY_LIST = {
181
181
  "America/North_Dakota/New_Salem": "US",
182
182
  "America/Nuuk": "GL",
183
183
  "America/Ojinaga": "MX",
184
- "America/Panama": "PA,CA,KY",
184
+ "America/Panama": "PA",
185
185
  "America/Pangnirtung": "CA",
186
186
  "America/Paramaribo": "SR",
187
187
  "America/Phoenix": "US,CA",
@@ -392,58 +392,58 @@ export const COUNTRY_LIST = {
392
392
  "Canada/Saskatchewan": "CA",
393
393
  "Canada/Yukon": "CA",
394
394
  // CET
395
- CET: "",
395
+ CET: "CET",
396
396
  // Chile
397
397
  "Chile/Continental": "CL",
398
398
  "Chile/EasterIsland": "CL",
399
399
  // CST6CDT
400
- CST6CDT: "",
400
+ CST6CDT: "CST6CDT",
401
401
  // Cuba
402
402
  Cuba: "CU",
403
403
  // EET
404
- EET: "",
404
+ EET: "EET",
405
405
  // Egypt
406
406
  Egypt: "EG",
407
407
  // Eire
408
408
  Eire: "IE",
409
409
  // EST
410
- EST: "",
411
- EST5EDT: "",
412
- "Etc/GMT": "",
413
- "Etc/GMT+0": "",
414
- "Etc/GMT+1": "",
415
- "Etc/GMT+10": "",
416
- "Etc/GMT+11": "",
417
- "Etc/GMT+12": "",
418
- "Etc/GMT+2": "",
419
- "Etc/GMT+3": "",
420
- "Etc/GMT+4": "",
421
- "Etc/GMT+5": "",
422
- "Etc/GMT+6": "",
423
- "Etc/GMT+7": "",
424
- "Etc/GMT+8": "",
425
- "Etc/GMT+9": "",
426
- "Etc/GMT-0": "",
427
- "Etc/GMT-1": "",
428
- "Etc/GMT-10": "",
429
- "Etc/GMT-11": "",
430
- "Etc/GMT-12": "",
431
- "Etc/GMT-13": "",
432
- "Etc/GMT-14": "",
433
- "Etc/GMT-2": "",
434
- "Etc/GMT-3": "",
435
- "Etc/GMT-4": "",
436
- "Etc/GMT-5": "",
437
- "Etc/GMT-6": "",
438
- "Etc/GMT-7": "",
439
- "Etc/GMT-8": "",
440
- "Etc/GMT-9": "",
441
- "Etc/GMT0": "",
442
- "Etc/Greenwich": "",
443
- "Etc/UCT": "",
444
- "Etc/UTC": "",
445
- "Etc/Universal": "",
446
- "Etc/Zulu": "",
410
+ EST: "EST",
411
+ EST5EDT: "EST5EDT",
412
+ "Etc/GMT": "Etc/GMT",
413
+ "Etc/GMT+0": "Etc/GMT+0",
414
+ "Etc/GMT+1": "Etc/GMT+1",
415
+ "Etc/GMT+10": "Etc/GMT+10",
416
+ "Etc/GMT+11": "Etc/GMT+11",
417
+ "Etc/GMT+12": "Etc/GMT+12",
418
+ "Etc/GMT+2": "Etc/GMT+2",
419
+ "Etc/GMT+3": "Etc/GMT+3",
420
+ "Etc/GMT+4": "Etc/GMT+4",
421
+ "Etc/GMT+5": "Etc/GMT+5",
422
+ "Etc/GMT+6": "Etc/GMT+6",
423
+ "Etc/GMT+7": "Etc/GMT+7",
424
+ "Etc/GMT+8": "Etc/GMT+8",
425
+ "Etc/GMT+9": "Etc/GMT+9",
426
+ "Etc/GMT-0": "Etc/GMT-0",
427
+ "Etc/GMT-1": "Etc/GMT-1",
428
+ "Etc/GMT-10": "Etc/GMT-10",
429
+ "Etc/GMT-11": "Etc/GMT-11",
430
+ "Etc/GMT-12": "Etc/GMT-12",
431
+ "Etc/GMT-13": "Etc/GMT-13",
432
+ "Etc/GMT-14": "Etc/GMT-14",
433
+ "Etc/GMT-2": "Etc/GMT-2",
434
+ "Etc/GMT-3": "Etc/GMT-3",
435
+ "Etc/GMT-4": "Etc/GMT-4",
436
+ "Etc/GMT-5": "Etc/GMT-5",
437
+ "Etc/GMT-6": "Etc/GMT-6",
438
+ "Etc/GMT-7": "Etc/GMT-7",
439
+ "Etc/GMT-8": "Etc/GMT-8",
440
+ "Etc/GMT-9": "Etc/GMT-9",
441
+ "Etc/GMT0": "Etc/GMT0",
442
+ "Etc/Greenwich": "Etc/Greenwich",
443
+ "Etc/UCT": "Etc/UCT",
444
+ "Etc/UTC": "Etc/UTC",
445
+ "Etc/Universal": "Etc/Universal",
446
+ "Etc/Zulu": "Etc/Zulu",
447
447
  // Europe
448
448
  "Europe/Amsterdam": "NL",
449
449
  "Europe/Andorra": "AD",
@@ -510,20 +510,20 @@ export const COUNTRY_LIST = {
510
510
  "Europe/Zaporozhye": "UA",
511
511
  "Europe/Zurich": "CH",
512
512
  // Factory
513
- Factory: "",
513
+ Factory: "Factory",
514
514
  // GB
515
515
  GB: "GB",
516
516
  "GB-Eire": "GB",
517
517
  // GMT
518
- GMT: "",
519
- "GMT+0": "",
520
- "GMT-0": "",
521
- GMT0: "",
522
- Greenwich: "",
518
+ GMT: "GMT",
519
+ "GMT+0": "GMT+0",
520
+ "GMT-0": "GMT-0",
521
+ GMT0: "GMT0",
522
+ Greenwich: "Greenwich",
523
523
  // HK
524
524
  Hongkong: "HK",
525
525
  // HST
526
- HST: "",
526
+ HST: "HST",
527
527
  // Iceland
528
528
  Iceland: "IS",
529
529
  // Indian
@@ -551,14 +551,14 @@ export const COUNTRY_LIST = {
551
551
  // Libya
552
552
  Libya: "LY",
553
553
  // MET
554
- MET: "",
554
+ MET: "MET",
555
555
  // Mexico
556
556
  "Mexico/BajaNorte": "MX",
557
557
  "Mexico/BajaSur": "MX",
558
558
  "Mexico/General": "MX",
559
559
  // MST
560
- MST: "",
561
- MST7MDT: "",
560
+ MST: "MST",
561
+ MST7MDT: "MST7MDT",
562
562
  // Navajo
563
563
  Navajo: "US",
564
564
  // NZ
@@ -616,7 +616,7 @@ export const COUNTRY_LIST = {
616
616
  // PRC
617
617
  PRC: "CN",
618
618
  // PST8PDT
619
- PST8PDT: "",
619
+ PST8PDT: "PST8PDT",
620
620
  // ROC
621
621
  ROC: "TW",
622
622
  // ROK
@@ -626,9 +626,9 @@ export const COUNTRY_LIST = {
626
626
  // Turkey
627
627
  Turkey: "TR",
628
628
  // UCT
629
- UCT: "",
629
+ UCT: "UCT",
630
630
  // Universal
631
- Universal: "",
631
+ Universal: "Universal",
632
632
  // US
633
633
  "US/Alaska": "US",
634
634
  "US/Aleutian": "US",
@@ -643,11 +643,11 @@ export const COUNTRY_LIST = {
643
643
  "US/Pacific": "US",
644
644
  "US/Samoa": "AS",
645
645
  // UTC
646
- UTC: "",
646
+ UTC: "UTC",
647
647
  // W-SU
648
648
  "W-SU": "RU",
649
649
  // WET
650
- WET: "",
650
+ WET: "WET",
651
651
  // Zulu
652
- Zulu: "",
652
+ Zulu: "Zulu",
653
653
  };
@@ -1,2 +1,4 @@
1
- export * from './config';
2
- export * from './events';
1
+ export * from "./base";
2
+ export * from "./config";
3
+ export * from "./events";
4
+ export * from "./regex";
@@ -0,0 +1,3 @@
1
+ export const REGEX = {
2
+ addressRegex: /^0x[a-fA-F0-9]{40}$/,
3
+ };
@@ -0,0 +1,9 @@
1
+ import { load } from "@fingerprintjs/fingerprintjs";
2
+
3
+ export class Fingerprint {
4
+ static async getVisitorId(): Promise<string> {
5
+ const fp = await load();
6
+ const { visitorId } = await fp.get();
7
+ return visitorId;
8
+ }
9
+ }
@@ -0,0 +1,2 @@
1
+ export { default as session } from "./session-storage";
2
+ export * from "./utils";
@@ -0,0 +1,310 @@
1
+ import fetch from "fetch-retry";
2
+ import isNetworkError from "is-network-error";
3
+ import { RequestEvent } from "../types";
4
+ import {
5
+ clampNumber,
6
+ getActionDescriptor,
7
+ millisecondsToSecond,
8
+ toDateHourMinute,
9
+ } from "./utils";
10
+ import { Fingerprint } from "./fingerprint";
11
+
12
+ const sdkFetch = fetch(global.fetch);
13
+
14
+ const noop = () => {};
15
+
16
+ type QueueItem = {
17
+ message: RequestEvent;
18
+ callback: (...args: any) => any;
19
+ };
20
+
21
+ type Options = {
22
+ url: string;
23
+ flushAt?: number;
24
+ flushInterval?: number;
25
+ host?: string;
26
+ retryCount?: number;
27
+ errorHandler?: any;
28
+ maxQueueSize?: number;
29
+ };
30
+
31
+ const DEFAULT_RETRY = 3;
32
+ const MAX_RETRY = 5;
33
+ const MIN_RETRY = 1;
34
+
35
+ const DEFAULT_FLUSH_AT = 20;
36
+ const MAX_FLUSH_AT = 20;
37
+ const MIN_FLUSH_AT = 1;
38
+
39
+ const DEFAULT_QUEUE_SIZE = 1_024 * 500; // 500kB
40
+ const MAX_QUEUE_SIZE = 1_024 * 500; // 500kB
41
+ const MIN_QUEUE_SIZE = 200; // 200 bytes
42
+
43
+ const DEFAULT_FLUSH_INTERVAL = 1_000 * 30; // 1 MINUTE
44
+ const MAX_FLUSH_INTERVAL = 1_000 * 300; // 5 MINUTES
45
+ const MIN_FLUSH_INTERVAL = 1_000 * 10; // 10 SECONDS
46
+
47
+ export class EventQueue {
48
+ private writeKey: string;
49
+ private url: string;
50
+ private queue: QueueItem[];
51
+ private timer: null | NodeJS.Timeout;
52
+ private flushAt: number;
53
+ private flushInterval: number;
54
+ private flushed: boolean;
55
+ private maxQueueSize: number; // min 200 bytes, max 500kB
56
+ private errorHandler: any;
57
+ private retryCount: number;
58
+ private pendingFlush: Promise<any> | null;
59
+ private payloadHashes: Set<string> = new Set();
60
+
61
+ constructor(writeKey: string, options: Options) {
62
+ options = options || {};
63
+
64
+ this.queue = [];
65
+ this.writeKey = writeKey;
66
+ this.url = options.url;
67
+ this.retryCount = clampNumber(
68
+ options.retryCount || DEFAULT_RETRY,
69
+ MAX_RETRY,
70
+ MIN_RETRY
71
+ );
72
+ this.flushAt = clampNumber(
73
+ options.flushAt || DEFAULT_FLUSH_AT,
74
+ MAX_FLUSH_AT,
75
+ MIN_FLUSH_AT
76
+ );
77
+ this.maxQueueSize = clampNumber(
78
+ options.maxQueueSize || DEFAULT_QUEUE_SIZE,
79
+ MAX_QUEUE_SIZE,
80
+ MIN_QUEUE_SIZE
81
+ );
82
+ this.flushInterval = clampNumber(
83
+ options.flushInterval || DEFAULT_FLUSH_INTERVAL,
84
+ MAX_FLUSH_INTERVAL,
85
+ MIN_FLUSH_INTERVAL
86
+ );
87
+ this.flushed = true;
88
+ this.errorHandler = options.errorHandler;
89
+ this.pendingFlush = null;
90
+ this.timer = null;
91
+
92
+ this.onPageLeave(async (isAccessible: boolean) => {
93
+ if (isAccessible === false) {
94
+ await this.flush();
95
+ }
96
+ });
97
+ }
98
+
99
+ //#region Public functions
100
+ async enqueue(message: RequestEvent, callback?: (...args: any) => void) {
101
+ callback = callback || noop;
102
+
103
+ // check if the message already exists
104
+ if (await this.checkDuplicate(message)) {
105
+ console.warn(
106
+ `Event already enqueued, try again after ${millisecondsToSecond(
107
+ this.flushInterval
108
+ )} seconds.`
109
+ );
110
+ return;
111
+ }
112
+
113
+ this.queue.push({ message, callback });
114
+
115
+ console.log(
116
+ `Event enqueued: ${getActionDescriptor(message.action, message.payload)}`
117
+ );
118
+
119
+ if (!this.flushed) {
120
+ this.flushed = true;
121
+ this.flush();
122
+ return;
123
+ }
124
+
125
+ const hasReachedFlushAt = this.queue.length >= this.flushAt;
126
+ const hasReachedQueueSize =
127
+ this.queue.reduce((acc, item) => acc + JSON.stringify(item).length, 0) >=
128
+ this.maxQueueSize;
129
+
130
+ if (hasReachedFlushAt || hasReachedQueueSize) {
131
+ this.flush();
132
+ return;
133
+ }
134
+
135
+ if (this.flushInterval && !this.timer) {
136
+ this.timer = setTimeout(this.flush.bind(this), this.flushInterval);
137
+ }
138
+ }
139
+
140
+ async flush(callback?: (...args: any) => void) {
141
+ callback = callback || noop;
142
+
143
+ if (this.timer) {
144
+ clearTimeout(this.timer);
145
+ this.timer = null;
146
+ }
147
+
148
+ if (!this.queue.length) {
149
+ callback();
150
+ return Promise.resolve();
151
+ }
152
+
153
+ try {
154
+ if (this.pendingFlush) {
155
+ await this.pendingFlush;
156
+ }
157
+ } catch (err) {
158
+ this.pendingFlush = null;
159
+ throw err;
160
+ }
161
+
162
+ const items = this.queue.splice(0, this.flushAt);
163
+ this.payloadHashes.clear();
164
+ const data = items.map((item) => item.message);
165
+
166
+ const done = (err?: Error) => {
167
+ items.forEach(({ message, callback }) => callback(err, message, data));
168
+ callback(err, data);
169
+ };
170
+
171
+ const req = {
172
+ headers: {
173
+ "Content-Type": "application/json",
174
+ Authorization: `Basic ${this.writeKey}`,
175
+ "X-Visitor-Id": await Fingerprint.getVisitorId(),
176
+ },
177
+ };
178
+
179
+ return (this.pendingFlush = sdkFetch(`${this.url}`, {
180
+ method: "POST",
181
+ body: JSON.stringify(data),
182
+ keepalive: true,
183
+ retries: this.retryCount,
184
+ retryDelay: (attempt) => Math.pow(2, attempt) * 1_000, // exponential backoff
185
+ retryOn: (_, error) => this.isErrorRetryable(error),
186
+ ...req,
187
+ })
188
+ .then(() => {
189
+ done();
190
+ return Promise.resolve(data);
191
+ })
192
+ .catch((err) => {
193
+ if (typeof this.errorHandler === "function") {
194
+ done(err);
195
+ return this.errorHandler(err);
196
+ }
197
+
198
+ if (err.response) {
199
+ const error = new Error(err.response.statusText);
200
+ done(error);
201
+ throw error;
202
+ }
203
+
204
+ done(err);
205
+ throw err;
206
+ }));
207
+ }
208
+
209
+ //#region Utility functions
210
+ private isErrorRetryable(error: any) {
211
+ // Retry Network Errors.
212
+ if (isNetworkError(error)) return true;
213
+
214
+ // Cannot determine if the request can be retried
215
+ if (!error?.response) return false;
216
+
217
+ // Retry Server Errors (5xx).
218
+ if (error?.response?.status >= 500 && error?.response?.status <= 599)
219
+ return true;
220
+
221
+ // Retry if rate limited.
222
+ if (error?.response?.status === 429) return true;
223
+
224
+ return false;
225
+ }
226
+
227
+ private async checkDuplicate(newMessage: RequestEvent) {
228
+ // check if exists a message with identical payload within 1 minute
229
+ const formattedTimestamp = toDateHourMinute(new Date(newMessage.timestamp));
230
+ newMessage.timestamp = formattedTimestamp;
231
+
232
+ const hash = await this.hashPayload(newMessage);
233
+ if (this.payloadHashes.has(hash)) {
234
+ return true;
235
+ }
236
+ this.payloadHashes.add(hash);
237
+ return false;
238
+ }
239
+
240
+ private async hashPayload(payload: RequestEvent, algo = "SHA-1") {
241
+ return Array.from(
242
+ new Uint8Array(
243
+ await crypto.subtle.digest(
244
+ algo,
245
+ new TextEncoder().encode(JSON.stringify(payload))
246
+ )
247
+ ),
248
+ (byte) => byte.toString(16).padStart(2, "0")
249
+ ).join("");
250
+ }
251
+
252
+ private onPageLeave = (callback: (isAccessible: boolean) => void) => {
253
+ // To ensure the callback is only called once even if more than one events
254
+ // are fired at once.
255
+ let pageLeft = false;
256
+ let isAccessible = false;
257
+
258
+ function handleOnLeave() {
259
+ if (pageLeft) {
260
+ return;
261
+ }
262
+
263
+ pageLeft = true;
264
+
265
+ callback(isAccessible);
266
+
267
+ // Reset pageLeft on the next tick
268
+ // to ensure callback executes for other listeners
269
+ // when closing an inactive browser tab.
270
+ setTimeout(() => {
271
+ pageLeft = false;
272
+ }, 0);
273
+ }
274
+
275
+ // Catches the unloading of the page (e.g., closing the tab or navigating away).
276
+ // Includes user actions like clicking a link, entering a new URL,
277
+ // refreshing the page, or closing the browser tab
278
+ // Note that 'pagehide' is not supported in IE.
279
+ // So, this is a fallback.
280
+ (globalThis as typeof window).addEventListener("beforeunload", () => {
281
+ isAccessible = false;
282
+ handleOnLeave();
283
+ });
284
+
285
+ (globalThis as typeof window).addEventListener("blur", () => {
286
+ isAccessible = true;
287
+ handleOnLeave();
288
+ });
289
+
290
+ (globalThis as typeof window).addEventListener("focus", () => {
291
+ pageLeft = false;
292
+ });
293
+
294
+ // Catches the page being hidden, including scenarios like closing the tab.
295
+ document.addEventListener("pagehide", () => {
296
+ isAccessible = document.visibilityState === "hidden";
297
+ handleOnLeave();
298
+ });
299
+
300
+ // Catches visibility changes, such as switching tabs or minimizing the browser.
301
+ document.addEventListener("visibilitychange", () => {
302
+ isAccessible = true;
303
+ if (document.visibilityState === "hidden") {
304
+ handleOnLeave();
305
+ } else {
306
+ pageLeft = false;
307
+ }
308
+ });
309
+ };
310
+ }
@@ -0,0 +1,53 @@
1
+ export class SessionStorage {
2
+ private readonly json_prefix = "__json=";
3
+
4
+ public set(key: string, value: any): void {
5
+ if (typeof value === "boolean") value = value === true ? "true" : "false";
6
+ if (typeof value === "object")
7
+ value = this.json_prefix + JSON.stringify(value);
8
+ sessionStorage.setItem(key, value);
9
+ }
10
+
11
+ public get(key: string): string | boolean | Record<any, any> | null {
12
+ const value = sessionStorage.getItem(key);
13
+
14
+ if (!value || typeof value !== "string") return null;
15
+ if (["null", "undefined"].some((item) => item == value)) return null;
16
+
17
+ if (value.startsWith(this.json_prefix)) {
18
+ try {
19
+ return JSON.parse(value.slice(7));
20
+ } catch (error) {
21
+ console.error(
22
+ "[FORMO_ERROR] SessionStorage failed to parse JSON",
23
+ error
24
+ );
25
+ return null;
26
+ }
27
+ }
28
+
29
+ if (["true", "false"].some((item) => item == value)) {
30
+ return JSON.parse(value);
31
+ }
32
+
33
+ return value;
34
+ }
35
+
36
+ public removeMatch(pattern: RegExp): void {
37
+ for (const key in sessionStorage) {
38
+ if (pattern.test(key)) {
39
+ this.remove(key);
40
+ }
41
+ }
42
+ }
43
+
44
+ public remove(key: string): void {
45
+ sessionStorage.removeItem(key);
46
+ }
47
+
48
+ public clear(): void {
49
+ sessionStorage.clear();
50
+ }
51
+ }
52
+
53
+ export default new SessionStorage();
package/src/lib/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { REGEX } from "../constants";
2
+
1
3
  const toSnake = (str: string) =>
2
4
  str
3
5
  .replace(/([a-z])([A-Z])/g, "$1_$2")
@@ -24,3 +26,32 @@ export function toSnakeCase(obj: any, omitKeys: string[] = []) {
24
26
 
25
27
  return convert(obj);
26
28
  }
29
+
30
+ export const isLocalhost = () =>
31
+ /^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*:)*?:?0*1$/.test(
32
+ window.location.hostname
33
+ ) || window.location.protocol === "file:";
34
+
35
+ export const isAddress = (address: string) => REGEX.addressRegex.test(address);
36
+
37
+ export const millisecondsToSecond = (milliseconds: number): number =>
38
+ Math.ceil(milliseconds / 1_000);
39
+
40
+ export const toDateHourMinute = (date: Date) =>
41
+ date.getUTCFullYear() +
42
+ "-" +
43
+ ("0" + (date.getUTCMonth() + 1)).slice(-2) +
44
+ "-" +
45
+ ("0" + date.getUTCDate()).slice(-2) +
46
+ " " +
47
+ ("0" + date.getUTCHours()).slice(-2) +
48
+ ":" +
49
+ ("0" + date.getUTCMinutes()).slice(-2);
50
+
51
+ export const clampNumber = (value: number, max: number, min: number) => {
52
+ return Math.min(Math.max(value, min), max);
53
+ };
54
+
55
+ export const getActionDescriptor = (action: string, payload: any): string => {
56
+ return `${action}${payload?.status ? ` ${payload?.status}` : ""}`;
57
+ };
package/src/types/base.ts CHANGED
@@ -1,22 +1,29 @@
1
1
  import { EIP1193Provider } from "./provider";
2
2
 
3
3
  // Decimal chain ID
4
- export type ChainID = number
4
+ export type ChainID = number;
5
5
 
6
6
  // Address (EVM, Solana, etc.)
7
- export type Address = string
7
+ export type Address = string;
8
8
 
9
9
  export interface Options {
10
10
  provider?: EIP1193Provider;
11
+ trackLocalhost?: boolean;
12
+
13
+ flushAt?: number;
14
+ flushInterval?: number;
15
+ retryCount?: number;
16
+ maxQueueSize?: number;
11
17
  }
12
18
 
13
19
  export interface FormoAnalyticsProviderProps {
14
- apiKey: string;
20
+ writeKey: string;
15
21
  options?: Options;
16
22
  disabled?: boolean;
17
23
  children: React.ReactNode;
18
24
  }
19
25
 
20
26
  export interface Config {
21
- apiKey: string;
22
- }
27
+ writeKey: string;
28
+ trackLocalhost?: boolean;
29
+ }
@@ -1,11 +1,19 @@
1
+ export interface RequestEvent {
2
+ action: string;
3
+ payload: Record<string, unknown>;
4
+ address: string | null;
5
+ timestamp: string;
6
+ version: string;
7
+ }
8
+
1
9
  export enum SignatureStatus {
2
- REQUESTED = 'requested',
3
- REJECTED = 'rejected',
4
- CONFIRMED = 'confirmed',
10
+ REQUESTED = "requested",
11
+ REJECTED = "rejected",
12
+ CONFIRMED = "confirmed",
5
13
  }
6
14
 
7
15
  export enum TransactionStatus {
8
- STARTED = 'started',
9
- REJECTED = 'rejected',
10
- BROADCASTED = 'broadcasted',
11
- }
16
+ STARTED = "started",
17
+ REJECTED = "rejected",
18
+ BROADCASTED = "broadcasted",
19
+ }