@formo/analytics 1.13.4-alpha.9 → 1.14.1

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 (92) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +2 -2
  2. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  3. package/dist/cjs/src/FormoAnalytics.js +63 -76
  4. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  5. package/dist/cjs/src/FormoAnalyticsEventQueue.d.ts +35 -0
  6. package/dist/cjs/src/FormoAnalyticsEventQueue.d.ts.map +1 -0
  7. package/dist/cjs/src/FormoAnalyticsEventQueue.js +272 -0
  8. package/dist/cjs/src/FormoAnalyticsEventQueue.js.map +1 -0
  9. package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -1
  10. package/dist/cjs/src/FormoAnalyticsProvider.js +4 -12
  11. package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -1
  12. package/dist/cjs/src/constants/index.d.ts +1 -0
  13. package/dist/cjs/src/constants/index.d.ts.map +1 -1
  14. package/dist/cjs/src/constants/index.js +1 -0
  15. package/dist/cjs/src/constants/index.js.map +1 -1
  16. package/dist/cjs/src/constants/regex.d.ts +4 -0
  17. package/dist/cjs/src/constants/regex.d.ts.map +1 -0
  18. package/dist/cjs/src/constants/regex.js +7 -0
  19. package/dist/cjs/src/constants/regex.js.map +1 -0
  20. package/dist/cjs/src/lib/index.d.ts +1 -1
  21. package/dist/cjs/src/lib/index.d.ts.map +1 -1
  22. package/dist/cjs/src/lib/index.js +2 -2
  23. package/dist/cjs/src/lib/index.js.map +1 -1
  24. package/dist/cjs/src/lib/session-storage.d.ts +3 -3
  25. package/dist/cjs/src/lib/session-storage.d.ts.map +1 -1
  26. package/dist/cjs/src/lib/session-storage.js +4 -4
  27. package/dist/cjs/src/lib/session-storage.js.map +1 -1
  28. package/dist/cjs/src/lib/utils.d.ts +1 -0
  29. package/dist/cjs/src/lib/utils.d.ts.map +1 -1
  30. package/dist/cjs/src/lib/utils.js +4 -1
  31. package/dist/cjs/src/lib/utils.js.map +1 -1
  32. package/dist/cjs/src/types/base.d.ts +4 -0
  33. package/dist/cjs/src/types/base.d.ts.map +1 -1
  34. package/dist/cjs/src/types/events.d.ts +7 -0
  35. package/dist/cjs/src/types/events.d.ts.map +1 -1
  36. package/dist/cjs/src/types/events.js.map +1 -1
  37. package/dist/cjs/test/lib.spec.js +8 -0
  38. package/dist/cjs/test/lib.spec.js.map +1 -1
  39. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  40. package/dist/esm/src/FormoAnalytics.d.ts +2 -2
  41. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  42. package/dist/esm/src/FormoAnalytics.js +64 -74
  43. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  44. package/dist/esm/src/FormoAnalyticsEventQueue.d.ts +35 -0
  45. package/dist/esm/src/FormoAnalyticsEventQueue.d.ts.map +1 -0
  46. package/dist/esm/src/FormoAnalyticsEventQueue.js +266 -0
  47. package/dist/esm/src/FormoAnalyticsEventQueue.js.map +1 -0
  48. package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -1
  49. package/dist/esm/src/FormoAnalyticsProvider.js +4 -12
  50. package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -1
  51. package/dist/esm/src/constants/index.d.ts +1 -0
  52. package/dist/esm/src/constants/index.d.ts.map +1 -1
  53. package/dist/esm/src/constants/index.js +1 -0
  54. package/dist/esm/src/constants/index.js.map +1 -1
  55. package/dist/esm/src/constants/regex.d.ts +4 -0
  56. package/dist/esm/src/constants/regex.d.ts.map +1 -0
  57. package/dist/esm/src/constants/regex.js +4 -0
  58. package/dist/esm/src/constants/regex.js.map +1 -0
  59. package/dist/esm/src/lib/index.d.ts +1 -1
  60. package/dist/esm/src/lib/index.d.ts.map +1 -1
  61. package/dist/esm/src/lib/index.js +1 -1
  62. package/dist/esm/src/lib/index.js.map +1 -1
  63. package/dist/esm/src/lib/session-storage.d.ts +3 -3
  64. package/dist/esm/src/lib/session-storage.d.ts.map +1 -1
  65. package/dist/esm/src/lib/session-storage.js +4 -4
  66. package/dist/esm/src/lib/session-storage.js.map +1 -1
  67. package/dist/esm/src/lib/utils.d.ts +1 -0
  68. package/dist/esm/src/lib/utils.d.ts.map +1 -1
  69. package/dist/esm/src/lib/utils.js +2 -0
  70. package/dist/esm/src/lib/utils.js.map +1 -1
  71. package/dist/esm/src/types/base.d.ts +4 -0
  72. package/dist/esm/src/types/base.d.ts.map +1 -1
  73. package/dist/esm/src/types/events.d.ts +7 -0
  74. package/dist/esm/src/types/events.d.ts.map +1 -1
  75. package/dist/esm/src/types/events.js.map +1 -1
  76. package/dist/esm/test/lib.spec.js +9 -1
  77. package/dist/esm/test/lib.spec.js.map +1 -1
  78. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  79. package/dist/index.umd.min.js +1 -1
  80. package/dist/index.umd.min.js.map +1 -1
  81. package/package.json +4 -2
  82. package/src/FormoAnalytics.ts +63 -61
  83. package/src/FormoAnalyticsEventQueue.ts +282 -0
  84. package/src/FormoAnalyticsProvider.tsx +0 -8
  85. package/src/constants/index.ts +1 -0
  86. package/src/constants/regex.ts +3 -0
  87. package/src/lib/index.ts +1 -1
  88. package/src/lib/session-storage.ts +4 -4
  89. package/src/lib/utils.ts +4 -0
  90. package/src/types/base.ts +5 -0
  91. package/src/types/events.ts +15 -7
  92. package/test/lib.spec.ts +13 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formo/analytics",
3
- "version": "1.13.4-alpha.9",
3
+ "version": "1.14.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/getformo/sdk.git"
@@ -21,7 +21,9 @@
21
21
  },
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "axios": "^1.7.8",
24
+ "@fingerprintjs/fingerprintjs": "^4.6.0",
25
+ "fetch-retry": "^6.0.0",
26
+ "is-network-error": "^1.1.0",
25
27
  "mipd": "^0.0.7"
26
28
  },
27
29
  "devDependencies": {
@@ -1,4 +1,3 @@
1
- import axios from "axios";
2
1
  import { createStore, EIP6963ProviderDetail } from "mipd";
3
2
  import {
4
3
  COUNTRY_LIST,
@@ -16,9 +15,11 @@ import {
16
15
  RPCError,
17
16
  SignatureStatus,
18
17
  TransactionStatus,
18
+ RequestEvent,
19
19
  } from "./types";
20
- import { formoSessionStorage, isLocalhost, toSnakeCase } from "./lib";
20
+ import { session, isLocalhost, toSnakeCase, isAddress } from "./lib";
21
21
  import { SESSION_IDENTIFIED_KEY } from "./constants";
22
+ import { FormoAnalyticsEventQueue } from "./FormoAnalyticsEventQueue";
22
23
 
23
24
  interface IFormoAnalytics {
24
25
  page(): void;
@@ -62,7 +63,8 @@ interface IFormoAnalytics {
62
63
  export class FormoAnalytics implements IFormoAnalytics {
63
64
  private _provider?: EIP1193Provider;
64
65
  private _providerListeners: Record<string, (...args: unknown[]) => void> = {};
65
- private sessionIdentified?: boolean = false;
66
+ private session: FormoAnalyticsSession;
67
+ private eventQueue: FormoAnalyticsEventQueue;
66
68
 
67
69
  config: Config;
68
70
  currentChainId?: ChainID;
@@ -74,11 +76,18 @@ export class FormoAnalytics implements IFormoAnalytics {
74
76
  ) {
75
77
  this.config = {
76
78
  apiKey,
77
- trackLocalhost: options.trackLocalhost,
79
+ trackLocalhost: options.trackLocalhost || false,
78
80
  };
79
81
 
80
- this.sessionIdentified =
81
- (formoSessionStorage.getItem(SESSION_IDENTIFIED_KEY) as boolean) ?? false;
82
+ this.session = new FormoAnalyticsSession();
83
+
84
+ this.eventQueue = new FormoAnalyticsEventQueue(this.config.apiKey, {
85
+ url: EVENTS_API_URL,
86
+ flushAt: options.flushAt,
87
+ retryCount: options.retryCount,
88
+ maxQueueSize: options.maxQueueSize,
89
+ flushInterval: options.flushInterval,
90
+ });
82
91
 
83
92
  // TODO: replace with eip6963
84
93
  const provider = options.provider || window?.ethereum;
@@ -282,15 +291,17 @@ export class FormoAnalytics implements IFormoAnalytics {
282
291
  providerName?: string;
283
292
  rdns?: string;
284
293
  }): Promise<void> {
285
- if (this.sessionIdentified === false) {
286
- this.sessionIdentified = true;
287
- formoSessionStorage.setItem(SESSION_IDENTIFIED_KEY, true);
288
- await this.trackEvent(Event.IDENTIFY, {
289
- address,
290
- providerName,
291
- rdns,
292
- });
293
- }
294
+ if (this.session.isIdentified())
295
+ return console.warn(
296
+ "FormoAnalytics::identify: Wallet already identified in this session"
297
+ );
298
+
299
+ this.session.identify();
300
+ await this.trackEvent(Event.IDENTIFY, {
301
+ address,
302
+ providerName,
303
+ rdns,
304
+ });
294
305
  }
295
306
 
296
307
  /**
@@ -309,7 +320,7 @@ export class FormoAnalytics implements IFormoAnalytics {
309
320
 
310
321
  private trackProvider(provider: EIP1193Provider): void {
311
322
  if (provider === this._provider) {
312
- console.log("Provider already tracked.");
323
+ console.warn("FormoAnalytics::trackProvider: Provider already tracked.");
313
324
  return;
314
325
  }
315
326
 
@@ -542,8 +553,8 @@ export class FormoAnalytics implements IFormoAnalytics {
542
553
  }
543
554
 
544
555
  private async trackFirstPageHit(): Promise<void> {
545
- if (formoSessionStorage.getItem(CURRENT_URL_KEY) === null) {
546
- formoSessionStorage.setItem(CURRENT_URL_KEY, window.location.href);
556
+ if (session.get(CURRENT_URL_KEY) === null) {
557
+ session.set(CURRENT_URL_KEY, window.location.href);
547
558
  }
548
559
 
549
560
  return this.trackPageHit();
@@ -569,40 +580,36 @@ export class FormoAnalytics implements IFormoAnalytics {
569
580
  }
570
581
 
571
582
  private async onLocationChange(): Promise<void> {
572
- const currentUrl = formoSessionStorage.getItem(CURRENT_URL_KEY);
583
+ const currentUrl = session.get(CURRENT_URL_KEY);
573
584
 
574
585
  if (currentUrl !== window.location.href) {
575
- formoSessionStorage.setItem(CURRENT_URL_KEY, window.location.href);
586
+ session.set(CURRENT_URL_KEY, window.location.href);
576
587
  this.trackPageHit();
577
588
  }
578
589
  }
579
590
 
580
591
  private trackPageHit(): void {
581
592
  const pathname = window.location.pathname;
582
- const href = window.location.href;
583
593
  const hash = window.location.hash;
584
594
 
585
595
  if (!this.config.trackLocalhost && isLocalhost()) {
586
596
  return console.warn(
587
- "[Formo] Ignoring event because website is running locally"
597
+ "FormoAnalytics::trackPageHit: Ignoring event because website is running locally"
588
598
  );
589
599
  }
590
600
 
591
601
  setTimeout(async () => {
592
602
  this.trackEvent(Event.PAGE, {
593
603
  pathname,
594
- href,
595
604
  hash,
596
605
  });
597
606
  }, 300);
598
607
  }
599
608
 
600
- // TODO: refactor this with event queue and flushing
601
- // https://linear.app/getformo/issue/P-835/sdk-refactor-retries-with-event-queue-and-batching
602
609
  private async trackEvent(action: string, payload: any): Promise<void> {
603
610
  const address = await this.getAddress();
604
611
 
605
- const requestData = {
612
+ const requestData: RequestEvent = {
606
613
  address,
607
614
  timestamp: new Date().toISOString(),
608
615
  action,
@@ -610,31 +617,11 @@ export class FormoAnalytics implements IFormoAnalytics {
610
617
  payload: await this.buildEventPayload(toSnakeCase(payload)),
611
618
  };
612
619
 
613
- try {
614
- const response = await axios.post(
615
- EVENTS_API_URL,
616
- JSON.stringify(requestData),
617
- {
618
- headers: {
619
- "Content-Type": "application/json",
620
- Authorization: `Bearer ${this.config.apiKey}`,
621
- },
622
- }
623
- );
624
-
625
- if (response.status >= 200 && response.status < 300) {
626
- console.log(
627
- `Event sent successfully: ${this.getActionDescriptor(
628
- action,
629
- payload
630
- )}`
631
- );
632
- } else {
633
- throw new Error(`Failed with status: ${response.status}`);
634
- }
635
- } catch (error) {
636
- console.error(`Event "${action}" failed. Error: ${error}`);
637
- }
620
+ this.eventQueue.enqueue(requestData, (err, _, data) => {
621
+ if (err) {
622
+ console.error(err);
623
+ } else console.log(`Events sent successfully: ${data.length} events`);
624
+ });
638
625
  }
639
626
 
640
627
  /*
@@ -655,8 +642,8 @@ export class FormoAnalytics implements IFormoAnalytics {
655
642
  }
656
643
 
657
644
  private async identifyAll(providers: EIP6963ProviderDetail[]): Promise<void> {
658
- for (const { provider, info } of providers) {
659
- try {
645
+ try {
646
+ for (const { provider, info } of providers) {
660
647
  const accounts = await this.getAccounts(provider);
661
648
  // Identify with accounts
662
649
  if (accounts && accounts.length > 0) {
@@ -675,9 +662,9 @@ export class FormoAnalytics implements IFormoAnalytics {
675
662
  rdns: info.rdns,
676
663
  });
677
664
  }
678
- } catch (err) {
679
- console.log("identifying all => err", err);
680
665
  }
666
+ } catch (err) {
667
+ console.log("identifying all => err", err);
681
668
  }
682
669
  }
683
670
 
@@ -687,7 +674,7 @@ export class FormoAnalytics implements IFormoAnalytics {
687
674
 
688
675
  private async getAddress(): Promise<Address | null> {
689
676
  if (this.currentConnectedAddress) return this.currentConnectedAddress;
690
- if (!this.provider) {
677
+ if (!this?.provider) {
691
678
  console.log("FormoAnalytics::getAddress: the provider is not set");
692
679
  return null;
693
680
  }
@@ -695,7 +682,7 @@ export class FormoAnalytics implements IFormoAnalytics {
695
682
  try {
696
683
  const accounts = await this.getAccounts();
697
684
  if (accounts && accounts.length > 0) {
698
- return accounts[0];
685
+ return isAddress(accounts[0]) ? accounts[0] : null;
699
686
  }
700
687
  } catch (err) {
701
688
  console.log("Failed to fetch accounts from provider:", err);
@@ -713,7 +700,7 @@ export class FormoAnalytics implements IFormoAnalytics {
713
700
  method: "eth_accounts",
714
701
  });
715
702
  if (!res || res.length === 0) return null;
716
- return res;
703
+ return res.filter(isAddress);
717
704
  } catch (err) {
718
705
  if ((err as any).code !== 4001) {
719
706
  console.log(
@@ -787,13 +774,15 @@ export class FormoAnalytics implements IFormoAnalytics {
787
774
  // common browser properties
788
775
  return {
789
776
  "user-agent": window.navigator.userAgent,
790
- origin: url.origin,
777
+ href: url.href,
791
778
  locale: language,
792
779
  location,
793
780
  referrer: document.referrer,
794
781
  utm_source: params.get("utm_source"),
795
782
  utm_medium: params.get("utm_medium"),
796
783
  utm_campaign: params.get("utm_campaign"),
784
+ utm_content: params.get("utm_content"),
785
+ utm_term: params.get("utm_term"),
797
786
  ref: params.get("ref"),
798
787
  ...eventSpecificPayload,
799
788
  };
@@ -846,8 +835,21 @@ export class FormoAnalytics implements IFormoAnalytics {
846
835
  value,
847
836
  };
848
837
  }
838
+ }
839
+
840
+ interface IFormoAnalyticsSession {
841
+ isIdentified(): boolean;
842
+ identify(): void;
843
+ }
844
+
845
+ class FormoAnalyticsSession implements IFormoAnalyticsSession {
846
+ constructor() {}
847
+
848
+ public isIdentified(): boolean {
849
+ return session.get(SESSION_IDENTIFIED_KEY) === true;
850
+ }
849
851
 
850
- private getActionDescriptor(action: string, payload: any): string {
851
- return `${action}${payload.status ? ` ${payload.status}` : ""}`;
852
+ public identify(): void {
853
+ session.set(SESSION_IDENTIFIED_KEY, true);
852
854
  }
853
855
  }
@@ -0,0 +1,282 @@
1
+ import fetch from "fetch-retry";
2
+ import isNetworkError from "is-network-error";
3
+ import FingerprintJS from "@fingerprintjs/fingerprintjs";
4
+ import { RequestEvent } from "./types";
5
+
6
+ const sdkFetch = fetch(global.fetch);
7
+
8
+ const noop = () => {};
9
+
10
+ type QueueItem = {
11
+ message: RequestEvent;
12
+ callback: (...args: any) => any;
13
+ };
14
+
15
+ type Options = {
16
+ url: string;
17
+ flushAt?: number;
18
+ flushInterval?: number;
19
+ host?: string;
20
+ retryCount?: number;
21
+ errorHandler?: any;
22
+ maxQueueSize?: number;
23
+ };
24
+
25
+ const DEFAULT_RETRY = 3;
26
+ const MAX_RETRY = 5;
27
+ const MIN_RETRY = 1;
28
+
29
+ const DEFAULT_FLUSH_AT = 20;
30
+ const MAX_FLUSH_AT = 20;
31
+ const MIN_FLUSH_AT = 1;
32
+
33
+ const DEFAULT_QUEUE_SIZE = 1_024 * 500; // 500kB
34
+ const MAX_QUEUE_SIZE = 1_024 * 500; // 500kB
35
+ const MIN_QUEUE_SIZE = 200; // 200 bytes
36
+
37
+ const DEFAULT_FLUSH_INTERVAL = 1_000 * 60; // 1 MINUTE
38
+ const MAX_FLUSH_INTERVAL = 1_000 * 300; // 5 MINUTES
39
+ const MIN_FLUSH_INTERVAL = 1_000 * 10; // 10 SECONDS
40
+
41
+ export class FormoAnalyticsEventQueue {
42
+ private writeKey: string;
43
+ private url: string;
44
+ private queue: QueueItem[];
45
+ private timer: null | NodeJS.Timeout;
46
+ private flushAt: number;
47
+ private flushInterval: number;
48
+ private flushed: boolean;
49
+ private maxQueueSize: number; // min 200 bytes, max 500kB
50
+ private errorHandler: any;
51
+ private retryCount: number;
52
+ private pendingFlush: Promise<any> | null;
53
+
54
+ constructor(writeKey: string, options: Options) {
55
+ options = options || {};
56
+
57
+ this.queue = [];
58
+ this.writeKey = writeKey;
59
+ this.url = options.url;
60
+ this.retryCount = this.getFormattedNumericParams(
61
+ options.retryCount || DEFAULT_RETRY,
62
+ MAX_RETRY,
63
+ MIN_RETRY
64
+ );
65
+ this.flushAt = this.getFormattedNumericParams(
66
+ options.flushAt || DEFAULT_FLUSH_AT,
67
+ MAX_FLUSH_AT,
68
+ MIN_FLUSH_AT
69
+ );
70
+ this.maxQueueSize = this.getFormattedNumericParams(
71
+ options.maxQueueSize || DEFAULT_QUEUE_SIZE,
72
+ MAX_QUEUE_SIZE,
73
+ MIN_QUEUE_SIZE
74
+ );
75
+ this.flushInterval = this.getFormattedNumericParams(
76
+ options.flushInterval || DEFAULT_FLUSH_INTERVAL,
77
+ MAX_FLUSH_INTERVAL,
78
+ MIN_FLUSH_INTERVAL
79
+ );
80
+ this.flushed = true;
81
+ this.errorHandler = options.errorHandler;
82
+ this.pendingFlush = null;
83
+ this.timer = null;
84
+
85
+ // flush before page close
86
+ window.addEventListener("beforeunload", async (e) => {
87
+ e.stopImmediatePropagation();
88
+ await this.flush();
89
+ });
90
+ }
91
+
92
+ //#region Public functions
93
+ enqueue(message: RequestEvent, callback?: (...args: any) => void) {
94
+ callback = callback || noop;
95
+
96
+ // check if the message already exists
97
+ if (this.checkDuplicate(message)) {
98
+ console.warn(
99
+ `Event already enqueued, try again after ${this.millisecondsToSecond(
100
+ this.flushInterval
101
+ )} seconds.`
102
+ );
103
+ return;
104
+ }
105
+
106
+ this.queue.push({ message, callback });
107
+ console.log(
108
+ `Event enqueued: ${this.getActionDescriptor(
109
+ message.action,
110
+ message.payload
111
+ )}`
112
+ );
113
+
114
+ if (!this.flushed) {
115
+ this.flushed = true;
116
+ this.flush();
117
+ return;
118
+ }
119
+
120
+ const hasReachedFlushAt = this.queue.length >= this.flushAt;
121
+ const hasReachedQueueSize =
122
+ this.queue.reduce((acc, item) => acc + JSON.stringify(item).length, 0) >=
123
+ this.maxQueueSize;
124
+
125
+ if (hasReachedFlushAt || hasReachedQueueSize) {
126
+ this.flush();
127
+ return;
128
+ }
129
+
130
+ if (this.flushInterval && !this.timer) {
131
+ this.timer = setTimeout(this.flush.bind(this), this.flushInterval);
132
+ }
133
+ }
134
+
135
+ async flush(callback?: (...args: any) => void) {
136
+ callback = callback || noop;
137
+
138
+ if (this.timer) {
139
+ clearTimeout(this.timer);
140
+ this.timer = null;
141
+ }
142
+
143
+ if (!this.queue.length) {
144
+ callback();
145
+ return Promise.resolve();
146
+ }
147
+
148
+ try {
149
+ if (this.pendingFlush) {
150
+ await this.pendingFlush;
151
+ }
152
+ } catch (err) {
153
+ this.pendingFlush = null;
154
+ throw err;
155
+ }
156
+
157
+ const items = this.queue.splice(0, this.flushAt);
158
+ const data = items.map((item) => item.message);
159
+
160
+ const done = (err?: Error) => {
161
+ items.forEach(({ message, callback }) => callback(err, message, data));
162
+ callback(err, data);
163
+ };
164
+
165
+ const req = {
166
+ headers: {
167
+ "Content-Type": "application/json",
168
+ Authorization: `Basic ${this.writeKey}`,
169
+ "X-Visitor-Id": await this.getVisitorId(),
170
+ },
171
+ };
172
+
173
+ return (this.pendingFlush = sdkFetch(`${this.url}`, {
174
+ method: "POST",
175
+ body: JSON.stringify(data),
176
+ retries: this.retryCount,
177
+ retryDelay: (attempt) => Math.pow(2, attempt) * 1_000, // exponential backoff
178
+ retryOn: (_, error) => this.isErrorRetryable(error),
179
+ ...req,
180
+ })
181
+ .then(() => {
182
+ done();
183
+ return Promise.resolve(data);
184
+ })
185
+ .catch((err) => {
186
+ if (typeof this.errorHandler === "function") {
187
+ done(err);
188
+ return this.errorHandler(err);
189
+ }
190
+
191
+ if (err.response) {
192
+ const error = new Error(err.response.statusText);
193
+ done(error);
194
+ throw error;
195
+ }
196
+
197
+ done(err);
198
+ throw err;
199
+ }));
200
+ }
201
+
202
+ //#endregion
203
+
204
+ //#region Utility functions
205
+
206
+ private async getVisitorId(): Promise<string> {
207
+ const fp = await FingerprintJS.load();
208
+ const { visitorId } = await fp.get();
209
+ return visitorId;
210
+ }
211
+
212
+ private isErrorRetryable(error: any) {
213
+ // Retry Network Errors.
214
+ if (isNetworkError(error)) return true;
215
+
216
+ // Cannot determine if the request can be retried
217
+ if (!error?.response) return false;
218
+
219
+ // Retry Server Errors (5xx).
220
+ if (error?.response?.status >= 500 && error?.response?.status <= 599)
221
+ return true;
222
+
223
+ // Retry if rate limited.
224
+ if (error?.response?.status === 429) return true;
225
+
226
+ return false;
227
+ }
228
+
229
+ private millisecondsToSecond(milliseconds: number): number {
230
+ return Math.ceil(milliseconds / 1_000);
231
+ }
232
+
233
+ private toDateHourMinute(date: Date) {
234
+ return (
235
+ date.getUTCFullYear() +
236
+ "-" +
237
+ ("0" + (date.getUTCMonth() + 1)).slice(-2) +
238
+ "-" +
239
+ ("0" + date.getUTCDate()).slice(-2) +
240
+ " " +
241
+ ("0" + date.getUTCHours()).slice(-2) +
242
+ ":" +
243
+ ("0" + date.getUTCMinutes()).slice(-2)
244
+ );
245
+ }
246
+
247
+ private checkDuplicate(newMessage: RequestEvent) {
248
+ // check if exists a message with identical payload within 1 minute
249
+ const formattedTimestamp = this.toDateHourMinute(
250
+ new Date(newMessage.timestamp)
251
+ );
252
+ const stringifiedMessage = JSON.stringify({
253
+ ...newMessage,
254
+ timestamp: formattedTimestamp,
255
+ });
256
+
257
+ return this.queue.some((item) => {
258
+ const { message } = item;
259
+ const formattedItemTimestamp = this.toDateHourMinute(
260
+ new Date(message.timestamp)
261
+ );
262
+ const stringifiedItem = JSON.stringify({
263
+ ...message,
264
+ timestamp: formattedItemTimestamp,
265
+ });
266
+
267
+ return stringifiedItem === stringifiedMessage;
268
+ });
269
+ }
270
+
271
+ private getActionDescriptor(action: string, payload: any): string {
272
+ return `${action}${payload?.status ? ` ${payload?.status}` : ""}`;
273
+ }
274
+
275
+ private getFormattedNumericParams(value: number, max: number, min: number) {
276
+ if (value < min) return min;
277
+ if (value > max) return max;
278
+
279
+ return value;
280
+ }
281
+ //#endregion
282
+ }
@@ -29,7 +29,6 @@ const InitializedAnalytics = ({
29
29
  children,
30
30
  }: FormoAnalyticsProviderProps) => {
31
31
  const [sdk, setSdk] = useState<FormoAnalytics | undefined>();
32
- const [isInitialized, setIsInitialized] = useState(false);
33
32
  const initializedStartedRef = useRef(false);
34
33
 
35
34
  const initializeFormoAnalytics = async (apiKey: string, options: any) => {
@@ -39,8 +38,6 @@ const InitializedAnalytics = ({
39
38
  console.log("FormoAnalytics SDK initialized successfully");
40
39
  } catch (error) {
41
40
  console.error("Failed to initialize FormoAnalytics SDK", error);
42
- } finally {
43
- setIsInitialized(true); // Ensure UI renders even after failure
44
41
  }
45
42
  };
46
43
 
@@ -55,11 +52,6 @@ const InitializedAnalytics = ({
55
52
  initialize();
56
53
  }, [apiKey, options]);
57
54
 
58
- if (!isInitialized) {
59
- // Optionally show a loading state until initialization attempt finishes
60
- return <div>Loading analytics...</div>;
61
- }
62
-
63
55
  return (
64
56
  <FormoAnalyticsContext.Provider value={sdk}>
65
57
  {children}
@@ -1,3 +1,4 @@
1
1
  export * from "./base";
2
2
  export * from "./config";
3
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
+ };
package/src/lib/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { default as formoSessionStorage } from "./session-storage";
1
+ export { default as session } from "./session-storage";
2
2
  export * from "./utils";
@@ -1,14 +1,14 @@
1
1
  export class SessionStorage {
2
2
  private readonly json_prefix = "__json=";
3
3
 
4
- public setItem(key: string, value: any): void {
4
+ public set(key: string, value: any): void {
5
5
  if (typeof value === "boolean") value = value === true ? "true" : "false";
6
6
  if (typeof value === "object")
7
7
  value = this.json_prefix + JSON.stringify(value);
8
8
  sessionStorage.setItem(key, value);
9
9
  }
10
10
 
11
- public getItem(key: string): string | boolean | Record<any, any> | null {
11
+ public get(key: string): string | boolean | Record<any, any> | null {
12
12
  const value = sessionStorage.getItem(key);
13
13
 
14
14
  if (!value || typeof value !== "string") return null;
@@ -36,12 +36,12 @@ export class SessionStorage {
36
36
  public removeMatch(pattern: RegExp): void {
37
37
  for (const key in sessionStorage) {
38
38
  if (pattern.test(key)) {
39
- this.removeItem(key);
39
+ this.remove(key);
40
40
  }
41
41
  }
42
42
  }
43
43
 
44
- public removeItem(key: string): void {
44
+ public remove(key: string): void {
45
45
  sessionStorage.removeItem(key);
46
46
  }
47
47
 
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")
@@ -29,3 +31,5 @@ export const isLocalhost = () =>
29
31
  /^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*:)*?:?0*1$/.test(
30
32
  window.location.hostname
31
33
  ) || window.location.protocol === "file:";
34
+
35
+ export const isAddress = (address: string) => REGEX.addressRegex.test(address);
package/src/types/base.ts CHANGED
@@ -9,6 +9,11 @@ export type Address = string;
9
9
  export interface Options {
10
10
  provider?: EIP1193Provider;
11
11
  trackLocalhost?: boolean;
12
+
13
+ flushAt?: number;
14
+ flushInterval?: number;
15
+ retryCount?: number;
16
+ maxQueueSize?: number;
12
17
  }
13
18
 
14
19
  export interface FormoAnalyticsProviderProps {
@@ -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
+ }