@formo/analytics 1.11.5-alpha.4 → 1.11.6
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/README.md +131 -39
- package/dist/cjs/src/FormoAnalytics.d.ts +72 -2
- package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
- package/dist/cjs/src/FormoAnalytics.js +282 -5
- package/dist/cjs/src/FormoAnalytics.js.map +1 -1
- package/dist/cjs/src/constants/events.d.ts +8 -0
- package/dist/cjs/src/constants/events.d.ts.map +1 -0
- package/dist/cjs/src/constants/events.js +12 -0
- package/dist/cjs/src/constants/events.js.map +1 -0
- 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/types/base.d.ts +1 -0
- package/dist/cjs/src/types/base.d.ts.map +1 -1
- package/dist/cjs/src/types/index.d.ts +1 -0
- package/dist/cjs/src/types/index.d.ts.map +1 -1
- package/dist/cjs/src/types/index.js +1 -0
- package/dist/cjs/src/types/index.js.map +1 -1
- package/dist/cjs/src/types/wallet.d.ts +11 -0
- package/dist/cjs/src/types/wallet.d.ts.map +1 -0
- package/dist/cjs/src/types/wallet.js +3 -0
- package/dist/cjs/src/types/wallet.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/src/FormoAnalytics.d.ts +72 -2
- package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
- package/dist/esm/src/FormoAnalytics.js +283 -6
- package/dist/esm/src/FormoAnalytics.js.map +1 -1
- package/dist/esm/src/constants/events.d.ts +8 -0
- package/dist/esm/src/constants/events.d.ts.map +1 -0
- package/dist/esm/src/constants/events.js +9 -0
- package/dist/esm/src/constants/events.js.map +1 -0
- 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/types/base.d.ts +1 -0
- package/dist/esm/src/types/base.d.ts.map +1 -1
- package/dist/esm/src/types/index.d.ts +1 -0
- package/dist/esm/src/types/index.d.ts.map +1 -1
- package/dist/esm/src/types/index.js +1 -0
- package/dist/esm/src/types/index.js.map +1 -1
- package/dist/esm/src/types/wallet.d.ts +11 -0
- package/dist/esm/src/types/wallet.d.ts.map +1 -0
- package/dist/esm/src/types/wallet.js +2 -0
- package/dist/esm/src/types/wallet.js.map +1 -0
- 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 +1 -1
- package/src/FormoAnalytics.ts +322 -6
- package/src/constants/events.ts +7 -0
- package/src/constants/index.ts +1 -0
- package/src/global.d.ts +4 -0
- package/src/types/base.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/wallet.ts +12 -0
package/package.json
CHANGED
package/src/FormoAnalytics.ts
CHANGED
|
@@ -1,19 +1,65 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
COUNTRY_LIST,
|
|
4
|
+
EVENTS_API,
|
|
5
|
+
SESSION_STORAGE_ID_KEY,
|
|
6
|
+
Event,
|
|
7
|
+
} from './constants';
|
|
3
8
|
import { isNotEmpty } from './utils';
|
|
4
9
|
import { H } from 'highlight.run';
|
|
10
|
+
import { ChainID, EIP1193Provider, RequestArguments } from './types';
|
|
5
11
|
|
|
6
12
|
interface IFormoAnalytics {
|
|
13
|
+
/**
|
|
14
|
+
* Initializes the FormoAnalytics instance with the provided API key and project ID.
|
|
15
|
+
*/
|
|
7
16
|
init(apiKey: string, projectId: string): Promise<FormoAnalytics>;
|
|
8
|
-
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Identifies the user with the provided user data.
|
|
20
|
+
*/
|
|
21
|
+
identify(userData: Record<string, any>): void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tracks page visit events.
|
|
25
|
+
*/
|
|
9
26
|
page(): void;
|
|
10
|
-
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Connects to a wallet with the specified chain ID and address.
|
|
30
|
+
*/
|
|
31
|
+
connect(params: { account: string; chainId: ChainID }): Promise<void>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Disconnects the current wallet and clears the session information.
|
|
35
|
+
*/
|
|
36
|
+
disconnect(attributes?: { account?: string; chainId?: ChainID }): void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tracks a specific event with a name and associated data.
|
|
40
|
+
*/
|
|
41
|
+
track(eventName: string, eventData: Record<string, any>): void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Switches the blockchain chain context and optionally logs additional attributes.
|
|
45
|
+
*/
|
|
46
|
+
chain(attributes: { chainId: ChainID; account?: string }): void;
|
|
11
47
|
}
|
|
12
48
|
export class FormoAnalytics implements IFormoAnalytics {
|
|
49
|
+
private _provider?: EIP1193Provider;
|
|
50
|
+
private _registeredProviderListeners: Record<
|
|
51
|
+
string,
|
|
52
|
+
(...args: unknown[]) => void
|
|
53
|
+
> = {};
|
|
54
|
+
|
|
55
|
+
private sessionKey = 'walletAddress';
|
|
13
56
|
private config: any;
|
|
14
57
|
private sessionIdKey: string = SESSION_STORAGE_ID_KEY;
|
|
15
58
|
private timezoneToCountry: Record<string, string> = COUNTRY_LIST;
|
|
16
59
|
|
|
60
|
+
currentChainId?: string | null;
|
|
61
|
+
currentConnectedAccount?: string;
|
|
62
|
+
|
|
17
63
|
private constructor(
|
|
18
64
|
public readonly apiKey: string,
|
|
19
65
|
public projectId: string
|
|
@@ -21,6 +67,11 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
21
67
|
this.config = {
|
|
22
68
|
token: this.apiKey,
|
|
23
69
|
};
|
|
70
|
+
|
|
71
|
+
const provider = window?.ethereum || window.web3?.currentProvider;
|
|
72
|
+
if (provider) {
|
|
73
|
+
this.trackProvider(provider);
|
|
74
|
+
}
|
|
24
75
|
}
|
|
25
76
|
|
|
26
77
|
static async init(
|
|
@@ -36,8 +87,12 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
36
87
|
return instance;
|
|
37
88
|
}
|
|
38
89
|
|
|
90
|
+
get provider(): EIP1193Provider | undefined {
|
|
91
|
+
return this._provider;
|
|
92
|
+
}
|
|
93
|
+
|
|
39
94
|
private identifyUser(userData: any) {
|
|
40
|
-
this.trackEvent(
|
|
95
|
+
this.trackEvent(Event.IDENTIFY, userData);
|
|
41
96
|
}
|
|
42
97
|
|
|
43
98
|
private getSessionId() {
|
|
@@ -83,10 +138,11 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
83
138
|
|
|
84
139
|
this.setSessionCookie(this.config.domain);
|
|
85
140
|
const apiUrl = this.buildApiUrl();
|
|
141
|
+
const address = await this.getCurrentWallet();
|
|
86
142
|
|
|
87
143
|
const requestData = {
|
|
88
144
|
project_id: this.projectId,
|
|
89
|
-
address:
|
|
145
|
+
address: address,
|
|
90
146
|
session_id: this.getSessionId(),
|
|
91
147
|
timestamp: new Date().toISOString(),
|
|
92
148
|
action: action,
|
|
@@ -220,7 +276,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
220
276
|
setTimeout(() => {
|
|
221
277
|
const url = new URL(window.location.href);
|
|
222
278
|
const params = new URLSearchParams(url.search);
|
|
223
|
-
this.trackEvent(
|
|
279
|
+
this.trackEvent(Event.PAGE, {
|
|
224
280
|
'user-agent': window.navigator.userAgent,
|
|
225
281
|
locale: language,
|
|
226
282
|
location: location,
|
|
@@ -235,6 +291,204 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
235
291
|
}, 300);
|
|
236
292
|
}
|
|
237
293
|
|
|
294
|
+
private trackProvider(provider: EIP1193Provider) {
|
|
295
|
+
if (provider === this._provider) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.currentChainId = undefined;
|
|
300
|
+
this.currentConnectedAccount = undefined;
|
|
301
|
+
|
|
302
|
+
if (this._provider) {
|
|
303
|
+
const eventNames = Object.keys(this._registeredProviderListeners);
|
|
304
|
+
for (const eventName of eventNames) {
|
|
305
|
+
this._provider.removeListener(
|
|
306
|
+
eventName,
|
|
307
|
+
this._registeredProviderListeners[eventName]
|
|
308
|
+
);
|
|
309
|
+
delete this._registeredProviderListeners[eventName];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this._provider = provider;
|
|
314
|
+
|
|
315
|
+
this.getCurrentWallet();
|
|
316
|
+
this.registerAccountsChangedListener();
|
|
317
|
+
this.registerChainChangedListener();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private registerChainChangedListener() {
|
|
321
|
+
const listener = (...args: unknown[]) =>
|
|
322
|
+
this.onChainChanged(args[0] as string);
|
|
323
|
+
this.provider?.on('chainChanged', listener);
|
|
324
|
+
this._registeredProviderListeners['chainChanged'] = listener;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private handleAccountDisconnected() {
|
|
328
|
+
if (!this.currentConnectedAccount) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const disconnectAttributes = {
|
|
333
|
+
address: this.currentConnectedAccount,
|
|
334
|
+
chainId: this.currentChainId,
|
|
335
|
+
};
|
|
336
|
+
this.currentChainId = undefined;
|
|
337
|
+
this.currentConnectedAccount = undefined;
|
|
338
|
+
this.clearWalletAddress();
|
|
339
|
+
|
|
340
|
+
return this.trackEvent(Event.DISCONNECT, disconnectAttributes);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async onChainChanged(chainIdHex: string) {
|
|
344
|
+
this.currentChainId = parseInt(chainIdHex).toString();
|
|
345
|
+
if (!this.currentConnectedAccount) {
|
|
346
|
+
if (!this.provider) {
|
|
347
|
+
console.error(
|
|
348
|
+
'error',
|
|
349
|
+
'FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported'
|
|
350
|
+
);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const res: string[] | null | undefined = await this.provider.request({
|
|
356
|
+
method: 'eth_accounts',
|
|
357
|
+
});
|
|
358
|
+
if (!res || res.length === 0) {
|
|
359
|
+
console.error(
|
|
360
|
+
'error',
|
|
361
|
+
'FormoAnalytics::onChainChanged: unable to get account. eth_accounts returned empty'
|
|
362
|
+
);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.currentConnectedAccount = res[0];
|
|
367
|
+
} catch (err) {
|
|
368
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
369
|
+
if ((err as any).code !== 4001) {
|
|
370
|
+
// 4001: The request is rejected by the user , see https://docs.metamask.io/wallet/reference/provider-api/#errors
|
|
371
|
+
console.error(
|
|
372
|
+
'error',
|
|
373
|
+
`FormoAnalytics::onChainChanged: unable to get account. eth_accounts threw an error`,
|
|
374
|
+
err
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return this.chain({
|
|
382
|
+
chainId: this.currentChainId,
|
|
383
|
+
account: this.currentConnectedAccount,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async onAccountsChanged(accounts: string[]) {
|
|
388
|
+
if (accounts.length > 0) {
|
|
389
|
+
const newAccount = accounts[0];
|
|
390
|
+
if (newAccount !== this.currentConnectedAccount) {
|
|
391
|
+
this.handleAccountConnected(newAccount);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
this.handleAccountDisconnected();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private registerAccountsChangedListener() {
|
|
399
|
+
const listener = (...args: unknown[]) =>
|
|
400
|
+
this.onAccountsChanged(args[0] as string[]);
|
|
401
|
+
|
|
402
|
+
this._provider?.on('accountsChanged', listener);
|
|
403
|
+
this._registeredProviderListeners['accountsChanged'] = listener;
|
|
404
|
+
|
|
405
|
+
const handleAccountDisconnected = this.handleAccountDisconnected.bind(this);
|
|
406
|
+
this._provider?.on('disconnect', handleAccountDisconnected);
|
|
407
|
+
this._registeredProviderListeners['disconnect'] = handleAccountDisconnected;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async getCurrentChainId(): Promise<string> {
|
|
411
|
+
if (!this.provider) {
|
|
412
|
+
console.error('FormoAnalytics::getCurrentChainId: provider not set');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const chainIdHex = await this.provider?.request<string>({
|
|
416
|
+
method: 'eth_chainId',
|
|
417
|
+
});
|
|
418
|
+
// Because we're connected, the chainId cannot be null
|
|
419
|
+
if (!chainIdHex) {
|
|
420
|
+
console.error(
|
|
421
|
+
`FormoAnalytics::getCurrentChainId: chainIdHex is: ${chainIdHex}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return parseInt(chainIdHex as string, 16).toString();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private async handleAccountConnected(account: string) {
|
|
429
|
+
if (account === this.currentConnectedAccount) {
|
|
430
|
+
// We have already reported this account
|
|
431
|
+
return;
|
|
432
|
+
} else {
|
|
433
|
+
this.currentConnectedAccount = account;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
this.currentChainId = await this.getCurrentChainId();
|
|
437
|
+
|
|
438
|
+
this.connect({ account, chainId: this.currentChainId });
|
|
439
|
+
this.storeWalletAddress(account);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private async getCurrentWallet() {
|
|
443
|
+
if (!this.provider) {
|
|
444
|
+
console.warn('FormoAnalytics::getCurrentWallet: the provider is not set');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const sessionData = sessionStorage.getItem(this.sessionKey);
|
|
448
|
+
|
|
449
|
+
if (!sessionData) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const parsedData = JSON.parse(sessionData);
|
|
454
|
+
const sessionExpiry = 30 * 60 * 1000; // 30 minutes
|
|
455
|
+
const currentTime = Date.now();
|
|
456
|
+
|
|
457
|
+
if (currentTime - parsedData.timestamp > sessionExpiry) {
|
|
458
|
+
console.warn('Session expired. Ignoring wallet address.');
|
|
459
|
+
sessionStorage.removeItem(this.sessionKey); // Clear expired session data
|
|
460
|
+
return '';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.handleAccountConnected(parsedData.address);
|
|
464
|
+
return parsedData.address || '';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Stores the wallet address in session storage when connected.
|
|
469
|
+
* @param address - The wallet address to store.
|
|
470
|
+
*/
|
|
471
|
+
private storeWalletAddress(address: string): void {
|
|
472
|
+
if (!address) {
|
|
473
|
+
console.error('No wallet address provided to store.');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const sessionData = {
|
|
478
|
+
address,
|
|
479
|
+
timestamp: Date.now(),
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
sessionStorage.setItem(this.sessionKey, JSON.stringify(sessionData));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Clears the wallet address from session storage when disconnected.
|
|
487
|
+
*/
|
|
488
|
+
private clearWalletAddress(): void {
|
|
489
|
+
sessionStorage.removeItem(this.sessionKey);
|
|
490
|
+
}
|
|
491
|
+
|
|
238
492
|
// Function to build the API URL
|
|
239
493
|
private buildApiUrl(): string {
|
|
240
494
|
const { host, proxy, token, dataSource = 'analytics_events' } = this.config;
|
|
@@ -253,6 +507,68 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
253
507
|
return 'Error: No token provided';
|
|
254
508
|
}
|
|
255
509
|
|
|
510
|
+
connect({ account, chainId }: { account: string; chainId: ChainID }) {
|
|
511
|
+
if (!chainId) {
|
|
512
|
+
throw new Error('FormoAnalytics::connect: chainId cannot be empty');
|
|
513
|
+
}
|
|
514
|
+
if (!account) {
|
|
515
|
+
throw new Error('FormoAnalytics::connect: account cannot be empty');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.currentChainId = chainId.toString();
|
|
519
|
+
this.currentConnectedAccount = account;
|
|
520
|
+
|
|
521
|
+
return this.trackEvent(Event.CONNECT, {
|
|
522
|
+
chainId,
|
|
523
|
+
address: account,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
disconnect(attributes?: { account?: string; chainId?: ChainID }) {
|
|
528
|
+
const account = attributes?.account || this.currentConnectedAccount;
|
|
529
|
+
if (!account) {
|
|
530
|
+
// We have most likely already reported this disconnection with the automatic
|
|
531
|
+
// `disconnect` detection
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const chainId = attributes?.chainId || this.currentChainId;
|
|
536
|
+
const eventAttributes = {
|
|
537
|
+
account,
|
|
538
|
+
...(chainId && { chainId }),
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
this.currentChainId = undefined;
|
|
542
|
+
this.currentConnectedAccount = undefined;
|
|
543
|
+
|
|
544
|
+
return this.trackEvent(Event.DISCONNECT, eventAttributes);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
chain({ chainId, account }: { chainId: ChainID; account?: string }) {
|
|
548
|
+
if (!chainId || Number(chainId) === 0) {
|
|
549
|
+
throw new Error('FormoAnalytics::chain: chainId cannot be empty or 0');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!account && !this.currentConnectedAccount) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
'FormoAnalytics::chain: account was empty and no previous account has been recorded. You can either pass an account or call connect() first'
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (isNaN(Number(chainId))) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
'FormoAnalytics::chain: chainId must be a valid hex or decimal number'
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this.currentChainId = chainId.toString();
|
|
565
|
+
|
|
566
|
+
return this.trackEvent(Event.CHAIN_CHANGED, {
|
|
567
|
+
chainId,
|
|
568
|
+
account: account || this.currentConnectedAccount,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
256
572
|
init(apiKey: string, projectId: string): Promise<FormoAnalytics> {
|
|
257
573
|
const instance = new FormoAnalytics(apiKey, projectId);
|
|
258
574
|
|
package/src/constants/index.ts
CHANGED
package/src/global.d.ts
CHANGED
package/src/types/base.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import EventEmitter from 'events'
|
|
2
|
+
|
|
3
|
+
export interface RequestArguments {
|
|
4
|
+
method: string
|
|
5
|
+
params?: unknown[] | Record<string, unknown>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EIP1193Provider extends EventEmitter {
|
|
9
|
+
request<T>(args: RequestArguments): Promise<T | null | undefined>
|
|
10
|
+
on(eventName: string | symbol, listener: (...args: unknown[]) => void): this
|
|
11
|
+
removeListener(eventName: string | symbol, listener: (...args: unknown[]) => void): this
|
|
12
|
+
}
|