@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.
Files changed (57) hide show
  1. package/README.md +131 -39
  2. package/dist/cjs/src/FormoAnalytics.d.ts +72 -2
  3. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  4. package/dist/cjs/src/FormoAnalytics.js +282 -5
  5. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  6. package/dist/cjs/src/constants/events.d.ts +8 -0
  7. package/dist/cjs/src/constants/events.d.ts.map +1 -0
  8. package/dist/cjs/src/constants/events.js +12 -0
  9. package/dist/cjs/src/constants/events.js.map +1 -0
  10. package/dist/cjs/src/constants/index.d.ts +1 -0
  11. package/dist/cjs/src/constants/index.d.ts.map +1 -1
  12. package/dist/cjs/src/constants/index.js +1 -0
  13. package/dist/cjs/src/constants/index.js.map +1 -1
  14. package/dist/cjs/src/types/base.d.ts +1 -0
  15. package/dist/cjs/src/types/base.d.ts.map +1 -1
  16. package/dist/cjs/src/types/index.d.ts +1 -0
  17. package/dist/cjs/src/types/index.d.ts.map +1 -1
  18. package/dist/cjs/src/types/index.js +1 -0
  19. package/dist/cjs/src/types/index.js.map +1 -1
  20. package/dist/cjs/src/types/wallet.d.ts +11 -0
  21. package/dist/cjs/src/types/wallet.d.ts.map +1 -0
  22. package/dist/cjs/src/types/wallet.js +3 -0
  23. package/dist/cjs/src/types/wallet.js.map +1 -0
  24. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  25. package/dist/esm/src/FormoAnalytics.d.ts +72 -2
  26. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  27. package/dist/esm/src/FormoAnalytics.js +283 -6
  28. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  29. package/dist/esm/src/constants/events.d.ts +8 -0
  30. package/dist/esm/src/constants/events.d.ts.map +1 -0
  31. package/dist/esm/src/constants/events.js +9 -0
  32. package/dist/esm/src/constants/events.js.map +1 -0
  33. package/dist/esm/src/constants/index.d.ts +1 -0
  34. package/dist/esm/src/constants/index.d.ts.map +1 -1
  35. package/dist/esm/src/constants/index.js +1 -0
  36. package/dist/esm/src/constants/index.js.map +1 -1
  37. package/dist/esm/src/types/base.d.ts +1 -0
  38. package/dist/esm/src/types/base.d.ts.map +1 -1
  39. package/dist/esm/src/types/index.d.ts +1 -0
  40. package/dist/esm/src/types/index.d.ts.map +1 -1
  41. package/dist/esm/src/types/index.js +1 -0
  42. package/dist/esm/src/types/index.js.map +1 -1
  43. package/dist/esm/src/types/wallet.d.ts +11 -0
  44. package/dist/esm/src/types/wallet.d.ts.map +1 -0
  45. package/dist/esm/src/types/wallet.js +2 -0
  46. package/dist/esm/src/types/wallet.js.map +1 -0
  47. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  48. package/dist/index.umd.min.js +1 -1
  49. package/dist/index.umd.min.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/FormoAnalytics.ts +322 -6
  52. package/src/constants/events.ts +7 -0
  53. package/src/constants/index.ts +1 -0
  54. package/src/global.d.ts +4 -0
  55. package/src/types/base.ts +2 -0
  56. package/src/types/index.ts +1 -0
  57. package/src/types/wallet.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formo/analytics",
3
- "version": "1.11.5-alpha.4",
3
+ "version": "1.11.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/getformo/sdk.git"
@@ -1,19 +1,65 @@
1
1
  import axios from 'axios';
2
- import { COUNTRY_LIST, EVENTS_API, SESSION_STORAGE_ID_KEY } from './constants';
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
- identify(userData: any): void;
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
- track(eventName: string, eventData: any): void;
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('identify', userData);
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: '', // TODO: get cached / session wallet 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('page_hit', {
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
 
@@ -0,0 +1,7 @@
1
+ export enum Event {
2
+ IDENTIFY = 'identify',
3
+ PAGE = 'page_hit',
4
+ CONNECT = 'connect',
5
+ DISCONNECT = 'disconnect',
6
+ CHAIN_CHANGED = 'chain_changed',
7
+ }
@@ -1 +1,2 @@
1
1
  export * from './config';
2
+ export * from './events';
package/src/global.d.ts CHANGED
@@ -2,6 +2,10 @@ declare global {
2
2
  interface Window {
3
3
  __nightmare?: boolean;
4
4
  Cypress?: any;
5
+ ethereum?: EIP1193Provider
6
+ web3?: {
7
+ currentProvider?: EIP1193Provider
8
+ }
5
9
  }
6
10
  }
7
11
 
package/src/types/base.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type ChainID = string | number
2
+
1
3
  export interface FormoAnalyticsProviderProps {
2
4
  apiKey: string;
3
5
  projectId: string;
@@ -1 +1,2 @@
1
1
  export * from './base';
2
+ export * from './wallet'
@@ -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
+ }