@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.
- package/dist/cjs/src/FormoAnalytics.d.ts +2 -2
- package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
- package/dist/cjs/src/FormoAnalytics.js +63 -76
- package/dist/cjs/src/FormoAnalytics.js.map +1 -1
- package/dist/cjs/src/FormoAnalyticsEventQueue.d.ts +35 -0
- package/dist/cjs/src/FormoAnalyticsEventQueue.d.ts.map +1 -0
- package/dist/cjs/src/FormoAnalyticsEventQueue.js +272 -0
- package/dist/cjs/src/FormoAnalyticsEventQueue.js.map +1 -0
- package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -1
- package/dist/cjs/src/FormoAnalyticsProvider.js +4 -12
- package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -1
- package/dist/cjs/src/constants/index.d.ts +1 -0
- package/dist/cjs/src/constants/index.d.ts.map +1 -1
- package/dist/cjs/src/constants/index.js +1 -0
- package/dist/cjs/src/constants/index.js.map +1 -1
- package/dist/cjs/src/constants/regex.d.ts +4 -0
- package/dist/cjs/src/constants/regex.d.ts.map +1 -0
- package/dist/cjs/src/constants/regex.js +7 -0
- package/dist/cjs/src/constants/regex.js.map +1 -0
- package/dist/cjs/src/lib/index.d.ts +1 -1
- package/dist/cjs/src/lib/index.d.ts.map +1 -1
- package/dist/cjs/src/lib/index.js +2 -2
- package/dist/cjs/src/lib/index.js.map +1 -1
- package/dist/cjs/src/lib/session-storage.d.ts +3 -3
- package/dist/cjs/src/lib/session-storage.d.ts.map +1 -1
- package/dist/cjs/src/lib/session-storage.js +4 -4
- package/dist/cjs/src/lib/session-storage.js.map +1 -1
- package/dist/cjs/src/lib/utils.d.ts +1 -0
- package/dist/cjs/src/lib/utils.d.ts.map +1 -1
- package/dist/cjs/src/lib/utils.js +4 -1
- package/dist/cjs/src/lib/utils.js.map +1 -1
- package/dist/cjs/src/types/base.d.ts +4 -0
- package/dist/cjs/src/types/base.d.ts.map +1 -1
- package/dist/cjs/src/types/events.d.ts +7 -0
- package/dist/cjs/src/types/events.d.ts.map +1 -1
- package/dist/cjs/src/types/events.js.map +1 -1
- package/dist/cjs/test/lib.spec.js +8 -0
- package/dist/cjs/test/lib.spec.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/src/FormoAnalytics.d.ts +2 -2
- package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
- package/dist/esm/src/FormoAnalytics.js +64 -74
- package/dist/esm/src/FormoAnalytics.js.map +1 -1
- package/dist/esm/src/FormoAnalyticsEventQueue.d.ts +35 -0
- package/dist/esm/src/FormoAnalyticsEventQueue.d.ts.map +1 -0
- package/dist/esm/src/FormoAnalyticsEventQueue.js +266 -0
- package/dist/esm/src/FormoAnalyticsEventQueue.js.map +1 -0
- package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -1
- package/dist/esm/src/FormoAnalyticsProvider.js +4 -12
- package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -1
- package/dist/esm/src/constants/index.d.ts +1 -0
- package/dist/esm/src/constants/index.d.ts.map +1 -1
- package/dist/esm/src/constants/index.js +1 -0
- package/dist/esm/src/constants/index.js.map +1 -1
- package/dist/esm/src/constants/regex.d.ts +4 -0
- package/dist/esm/src/constants/regex.d.ts.map +1 -0
- package/dist/esm/src/constants/regex.js +4 -0
- package/dist/esm/src/constants/regex.js.map +1 -0
- package/dist/esm/src/lib/index.d.ts +1 -1
- package/dist/esm/src/lib/index.d.ts.map +1 -1
- package/dist/esm/src/lib/index.js +1 -1
- package/dist/esm/src/lib/index.js.map +1 -1
- package/dist/esm/src/lib/session-storage.d.ts +3 -3
- package/dist/esm/src/lib/session-storage.d.ts.map +1 -1
- package/dist/esm/src/lib/session-storage.js +4 -4
- package/dist/esm/src/lib/session-storage.js.map +1 -1
- package/dist/esm/src/lib/utils.d.ts +1 -0
- package/dist/esm/src/lib/utils.d.ts.map +1 -1
- package/dist/esm/src/lib/utils.js +2 -0
- package/dist/esm/src/lib/utils.js.map +1 -1
- package/dist/esm/src/types/base.d.ts +4 -0
- package/dist/esm/src/types/base.d.ts.map +1 -1
- package/dist/esm/src/types/events.d.ts +7 -0
- package/dist/esm/src/types/events.d.ts.map +1 -1
- package/dist/esm/src/types/events.js.map +1 -1
- package/dist/esm/test/lib.spec.js +9 -1
- package/dist/esm/test/lib.spec.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/index.umd.min.js +1 -1
- package/dist/index.umd.min.js.map +1 -1
- package/package.json +4 -2
- package/src/FormoAnalytics.ts +63 -61
- package/src/FormoAnalyticsEventQueue.ts +282 -0
- package/src/FormoAnalyticsProvider.tsx +0 -8
- package/src/constants/index.ts +1 -0
- package/src/constants/regex.ts +3 -0
- package/src/lib/index.ts +1 -1
- package/src/lib/session-storage.ts +4 -4
- package/src/lib/utils.ts +4 -0
- package/src/types/base.ts +5 -0
- package/src/types/events.ts +15 -7
- 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.
|
|
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
|
-
"
|
|
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": {
|
package/src/FormoAnalytics.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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.
|
|
81
|
-
|
|
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.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
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 (
|
|
546
|
-
|
|
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 =
|
|
583
|
+
const currentUrl = session.get(CURRENT_URL_KEY);
|
|
573
584
|
|
|
574
585
|
if (currentUrl !== window.location.href) {
|
|
575
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
851
|
-
|
|
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}
|
package/src/constants/index.ts
CHANGED
package/src/lib/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default as
|
|
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
|
|
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
|
|
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.
|
|
39
|
+
this.remove(key);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
public
|
|
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 {
|
package/src/types/events.ts
CHANGED
|
@@ -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 =
|
|
3
|
-
REJECTED =
|
|
4
|
-
CONFIRMED =
|
|
10
|
+
REQUESTED = "requested",
|
|
11
|
+
REJECTED = "rejected",
|
|
12
|
+
CONFIRMED = "confirmed",
|
|
5
13
|
}
|
|
6
14
|
|
|
7
15
|
export enum TransactionStatus {
|
|
8
|
-
STARTED =
|
|
9
|
-
REJECTED =
|
|
10
|
-
BROADCASTED =
|
|
11
|
-
}
|
|
16
|
+
STARTED = "started",
|
|
17
|
+
REJECTED = "rejected",
|
|
18
|
+
BROADCASTED = "broadcasted",
|
|
19
|
+
}
|