@formo/analytics 1.15.1 → 1.16.0

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 (55) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +9 -3
  2. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  3. package/dist/cjs/src/FormoAnalytics.js +114 -122
  4. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  5. package/dist/cjs/src/constants/base.d.ts +1 -1
  6. package/dist/cjs/src/constants/base.d.ts.map +1 -1
  7. package/dist/cjs/src/constants/base.js +2 -2
  8. package/dist/cjs/src/constants/base.js.map +1 -1
  9. package/dist/cjs/src/constants/events.d.ts +1 -0
  10. package/dist/cjs/src/constants/events.d.ts.map +1 -1
  11. package/dist/cjs/src/constants/events.js +1 -0
  12. package/dist/cjs/src/constants/events.js.map +1 -1
  13. package/dist/cjs/src/lib/logger.d.ts +8 -1
  14. package/dist/cjs/src/lib/logger.d.ts.map +1 -1
  15. package/dist/cjs/src/lib/logger.js +15 -5
  16. package/dist/cjs/src/lib/logger.js.map +1 -1
  17. package/dist/cjs/src/lib/queue.d.ts +2 -2
  18. package/dist/cjs/src/lib/queue.d.ts.map +1 -1
  19. package/dist/cjs/src/lib/queue.js +14 -13
  20. package/dist/cjs/src/lib/queue.js.map +1 -1
  21. package/dist/cjs/src/types/events.d.ts +1 -1
  22. package/dist/cjs/src/types/events.d.ts.map +1 -1
  23. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  24. package/dist/esm/src/FormoAnalytics.d.ts +9 -3
  25. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  26. package/dist/esm/src/FormoAnalytics.js +117 -125
  27. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  28. package/dist/esm/src/constants/base.d.ts +1 -1
  29. package/dist/esm/src/constants/base.d.ts.map +1 -1
  30. package/dist/esm/src/constants/base.js +1 -1
  31. package/dist/esm/src/constants/base.js.map +1 -1
  32. package/dist/esm/src/constants/events.d.ts +1 -0
  33. package/dist/esm/src/constants/events.d.ts.map +1 -1
  34. package/dist/esm/src/constants/events.js +1 -0
  35. package/dist/esm/src/constants/events.js.map +1 -1
  36. package/dist/esm/src/lib/logger.d.ts +8 -1
  37. package/dist/esm/src/lib/logger.d.ts.map +1 -1
  38. package/dist/esm/src/lib/logger.js +15 -5
  39. package/dist/esm/src/lib/logger.js.map +1 -1
  40. package/dist/esm/src/lib/queue.d.ts +2 -2
  41. package/dist/esm/src/lib/queue.d.ts.map +1 -1
  42. package/dist/esm/src/lib/queue.js +14 -13
  43. package/dist/esm/src/lib/queue.js.map +1 -1
  44. package/dist/esm/src/types/events.d.ts +1 -1
  45. package/dist/esm/src/types/events.d.ts.map +1 -1
  46. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  47. package/dist/index.umd.min.js +1 -1
  48. package/dist/index.umd.min.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/FormoAnalytics.ts +97 -84
  51. package/src/constants/base.ts +3 -2
  52. package/src/constants/events.ts +8 -7
  53. package/src/lib/logger.ts +28 -6
  54. package/src/lib/queue.ts +12 -11
  55. package/src/types/events.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formo/analytics",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/getformo/sdk.git"
@@ -6,8 +6,6 @@ import {
6
6
  EVENTS_API_URL,
7
7
  Event,
8
8
  SESSION_USER_ID_KEY,
9
- EVENTS_API_REQUEST_HEADER,
10
- USER_API_URL,
11
9
  } from "./constants";
12
10
  import {
13
11
  ChainID,
@@ -21,14 +19,14 @@ import {
21
19
  TransactionStatus,
22
20
  RequestEvent,
23
21
  } from "./types";
24
- import { session, local, logger, EventQueue, fetch } from "./lib";
22
+ import { session, local, logger, EventQueue, Logger } from "./lib";
25
23
  import {
26
24
  isLocalhost,
27
25
  isAddress,
28
26
  toSnakeCase,
29
27
  generateNativeUUID,
30
28
  } from "./utils";
31
- import { SESSION_IDENTIFIED_KEY } from "./constants";
29
+ import { SESSION_WALLET_DETECTED_KEY } from "./constants";
32
30
  import { UUID } from "crypto";
33
31
 
34
32
  interface IFormoAnalytics {
@@ -77,7 +75,7 @@ export class FormoAnalytics implements IFormoAnalytics {
77
75
  private session: FormoAnalyticsSession;
78
76
  private eventQueue: EventQueue;
79
77
  private anonymousId: UUID | null = null;
80
- private userId: UUID | null = null;
78
+ private userId: string | null = null;
81
79
 
82
80
  config: Config;
83
81
  currentChainId?: ChainID;
@@ -95,13 +93,10 @@ export class FormoAnalytics implements IFormoAnalytics {
95
93
  this.session = new FormoAnalyticsSession();
96
94
 
97
95
  // Initialize logger with configuration from options
98
- const loggerConfig = options.logger || {
99
- enabled: false,
100
- };
101
- logger.setEnabled(loggerConfig.enabled);
102
- if (loggerConfig.levels) {
103
- logger.setEnabledLevels(loggerConfig.levels);
104
- }
96
+ Logger.init({
97
+ enabled: options.logger?.enabled || false,
98
+ enabledLevels: options.logger?.levels || [],
99
+ });
105
100
 
106
101
  this.eventQueue = new EventQueue(this.config.writeKey, {
107
102
  url: EVENTS_API_URL,
@@ -112,7 +107,7 @@ export class FormoAnalytics implements IFormoAnalytics {
112
107
  });
113
108
 
114
109
  this.anonymousId = this.getAnonymousId();
115
- this.getUserId(null).then((userId) => (this.userId = userId));
110
+ this.userId = session.get(SESSION_USER_ID_KEY) as string | null;
116
111
 
117
112
  // TODO: replace with eip6963
118
113
  const provider = options.provider || window?.ethereum;
@@ -130,9 +125,9 @@ export class FormoAnalytics implements IFormoAnalytics {
130
125
  ): Promise<FormoAnalytics> {
131
126
  const analytics = new FormoAnalytics(writeKey, options);
132
127
 
133
- // Identify
128
+ // Detect
134
129
  const providers = await analytics.getProviders();
135
- await analytics.identifyAll(providers);
130
+ await analytics.detectWallets(providers);
136
131
 
137
132
  return analytics;
138
133
  }
@@ -321,16 +316,15 @@ export class FormoAnalytics implements IFormoAnalytics {
321
316
  public async identify({
322
317
  address,
323
318
  providerName,
319
+ userId,
324
320
  rdns,
325
321
  }: {
326
322
  address: Address | null;
327
323
  providerName?: string;
324
+ userId?: string;
328
325
  rdns?: string;
329
326
  }): Promise<void> {
330
- if (this.session.isIdentified())
331
- return logger.warn("Identify: Wallet already identified in this session");
332
-
333
- this.session.identify();
327
+ if (userId) this.userId = userId || null;
334
328
  await this.trackEvent(Event.IDENTIFY, {
335
329
  address,
336
330
  providerName,
@@ -338,6 +332,30 @@ export class FormoAnalytics implements IFormoAnalytics {
338
332
  });
339
333
  }
340
334
 
335
+ /**
336
+ * Emits an identify event with current wallet address.
337
+ * @param {Address} params.address
338
+ * @returns {Promise<void>}
339
+ */
340
+ private async detectWallet({
341
+ providerName,
342
+ rdns,
343
+ }: {
344
+ providerName: string;
345
+ rdns: string;
346
+ }): Promise<void> {
347
+ if (this.session.isWalletDetected(rdns))
348
+ return logger.warn(
349
+ `detectWallet: Wallet ${providerName} already detected in this session`
350
+ );
351
+
352
+ this.session.markWalletdetected(rdns);
353
+ await this.trackEvent(Event.DETECT_WALLET, {
354
+ providerName,
355
+ rdns,
356
+ });
357
+ }
358
+
341
359
  /**
342
360
  * Emits a custom event with custom data.
343
361
  * @param {string} action
@@ -498,7 +516,6 @@ export class FormoAnalytics implements IFormoAnalytics {
498
516
 
499
517
  return;
500
518
  } catch (error) {
501
- logger.info("Transaction listener catch");
502
519
  logger.error("Transaction error:", error);
503
520
  const rpcError = error as RPCError;
504
521
  if (rpcError && rpcError?.code === 4001) {
@@ -577,7 +594,6 @@ export class FormoAnalytics implements IFormoAnalytics {
577
594
  return Promise.resolve();
578
595
  }
579
596
  this.currentConnectedAddress = address;
580
- this.userId = await this.getUserId(address);
581
597
  }
582
598
 
583
599
  // Proceed only if the address exists
@@ -650,12 +666,16 @@ export class FormoAnalytics implements IFormoAnalytics {
650
666
  private async trackEvent(action: string, payload: any): Promise<void> {
651
667
  try {
652
668
  const address = await this.getAddress();
653
- const user_id = await this.getUserId(address);
669
+ const user_id = this.userId;
670
+
671
+ if (payload?.userId) {
672
+ delete payload.userId;
673
+ }
654
674
 
655
675
  const requestData: RequestEvent = {
656
676
  anonymous_id: this.anonymousId as UUID,
657
677
  user_id,
658
- address,
678
+ address: address?.toLowerCase() || null,
659
679
  timestamp: new Date().toISOString(),
660
680
  action,
661
681
  version: "1",
@@ -689,38 +709,54 @@ export class FormoAnalytics implements IFormoAnalytics {
689
709
  return providers;
690
710
  }
691
711
 
692
- private async identifyAll(
712
+ private async detectWallets(
693
713
  providers: readonly EIP6963ProviderDetail[]
694
714
  ): Promise<void> {
695
715
  try {
696
716
  for (const eip6963ProviderDetail of providers) {
697
- if (!eip6963ProviderDetail) continue;
698
- const accounts = await this.getAccounts(
699
- eip6963ProviderDetail?.provider
700
- );
701
- // Identify with accounts
702
- if (accounts && accounts.length > 0) {
703
- for (const address of accounts) {
704
- await this.identify({
705
- address,
706
- providerName: eip6963ProviderDetail?.info.name,
707
- rdns: eip6963ProviderDetail?.info.rdns,
708
- });
709
- }
710
- } else {
711
- // Identify without accounts
712
- await this.identify({
713
- address: null,
714
- providerName: eip6963ProviderDetail?.info.name,
715
- rdns: eip6963ProviderDetail?.info.rdns,
716
- });
717
- }
717
+ await this.detectWallet({
718
+ providerName: eip6963ProviderDetail?.info.name,
719
+ rdns: eip6963ProviderDetail?.info.rdns,
720
+ });
718
721
  }
719
722
  } catch (err) {
720
- logger.error("Error identifying all:", err);
723
+ logger.error("Error detect all wallets:", err);
721
724
  }
722
725
  }
723
726
 
727
+ // TODO: Refactoring => public this function as API
728
+ // private async identifyAll(
729
+ // providers: readonly EIP6963ProviderDetail[]
730
+ // ): Promise<void> {
731
+ // try {
732
+ // for (const eip6963ProviderDetail of providers) {
733
+ // if (!eip6963ProviderDetail) continue;
734
+ // const accounts = await this.getAccounts(
735
+ // eip6963ProviderDetail?.provider
736
+ // );
737
+ // // Identify with accounts
738
+ // if (accounts && accounts.length > 0) {
739
+ // for (const address of accounts) {
740
+ // await this.identify({
741
+ // address,
742
+ // providerName: eip6963ProviderDetail?.info.name,
743
+ // rdns: eip6963ProviderDetail?.info.rdns,
744
+ // });
745
+ // }
746
+ // } else {
747
+ // // Identify without accounts
748
+ // await this.identify({
749
+ // address: null,
750
+ // providerName: eip6963ProviderDetail?.info.name,
751
+ // rdns: eip6963ProviderDetail?.info.rdns,
752
+ // });
753
+ // }
754
+ // }
755
+ // } catch (err) {
756
+ // logger.error("Error identifying all:", err);
757
+ // }
758
+ // }
759
+
724
760
  get provider(): EIP1193Provider | undefined {
725
761
  return this._provider;
726
762
  }
@@ -734,31 +770,6 @@ export class FormoAnalytics implements IFormoAnalytics {
734
770
  return newAnonymousId;
735
771
  }
736
772
 
737
- private async getUserId(address: string | null): Promise<UUID | null> {
738
- const storedUserId = session.get(SESSION_USER_ID_KEY);
739
- if (storedUserId && typeof storedUserId === "string")
740
- return storedUserId as UUID;
741
-
742
- if (address) {
743
- const res = await fetch(`${USER_API_URL}?address=${address}`, {
744
- headers: EVENTS_API_REQUEST_HEADER(this.writeKey),
745
- method: "GET",
746
- });
747
- const data = await res.json();
748
- const userId = data?.data?.[0]?.user_id;
749
- if (userId) {
750
- session.set(SESSION_USER_ID_KEY, userId);
751
- return userId;
752
- }
753
-
754
- const newUserId = generateNativeUUID();
755
- session.set(SESSION_USER_ID_KEY, newUserId);
756
- return newUserId;
757
- }
758
-
759
- return null;
760
- }
761
-
762
773
  private async getAddress(): Promise<Address | null> {
763
774
  if (this.currentConnectedAddress) return this.currentConnectedAddress;
764
775
  if (!this?.provider) {
@@ -769,7 +780,6 @@ export class FormoAnalytics implements IFormoAnalytics {
769
780
  try {
770
781
  const accounts = await this.getAccounts();
771
782
  if (accounts && accounts.length > 0) {
772
- // TODO: fetch userId if account is valid, generate userId if no userId matches address from tinybird
773
783
  if (isAddress(accounts[0])) {
774
784
  return accounts[0];
775
785
  }
@@ -865,12 +875,12 @@ export class FormoAnalytics implements IFormoAnalytics {
865
875
  locale: language,
866
876
  location,
867
877
  referrer: document.referrer,
868
- utm_source: params.get("utm_source"),
869
- utm_medium: params.get("utm_medium"),
870
- utm_campaign: params.get("utm_campaign"),
871
- utm_content: params.get("utm_content"),
872
- utm_term: params.get("utm_term"),
873
- ref: params.get("ref"),
878
+ utm_source: params.get("utm_source")?.trim() || "",
879
+ utm_medium: params.get("utm_medium")?.trim() || "",
880
+ utm_campaign: params.get("utm_campaign")?.trim() || "",
881
+ utm_content: params.get("utm_content")?.trim() || "",
882
+ utm_term: params.get("utm_term")?.trim() || "",
883
+ ref: params.get("ref")?.trim() || "",
874
884
  ...eventSpecificPayload,
875
885
  };
876
886
  }
@@ -925,18 +935,21 @@ export class FormoAnalytics implements IFormoAnalytics {
925
935
  }
926
936
 
927
937
  interface IFormoAnalyticsSession {
928
- isIdentified(): boolean;
929
- identify(): void;
938
+ isWalletDetected(rdns: string): boolean;
939
+ markWalletdetected(rdns: string): void;
930
940
  }
931
941
 
932
942
  class FormoAnalyticsSession implements IFormoAnalyticsSession {
933
943
  constructor() {}
934
944
 
935
- public isIdentified(): boolean {
936
- return session.get(SESSION_IDENTIFIED_KEY) === true;
945
+ public isWalletDetected(rdns: string): boolean {
946
+ const rdnses = (session.get(SESSION_WALLET_DETECTED_KEY) as string[]) || [];
947
+ return rdnses.includes(rdns);
937
948
  }
938
949
 
939
- public identify(): void {
940
- session.set(SESSION_IDENTIFIED_KEY, true);
950
+ public markWalletdetected(rdns: string): void {
951
+ const rdnses = (session.get(SESSION_WALLET_DETECTED_KEY) as string[]) || [];
952
+ rdnses.push(rdns);
953
+ session.set(SESSION_WALLET_DETECTED_KEY, rdnses);
941
954
  }
942
955
  }
@@ -2,8 +2,9 @@ const STORAGE_PREFIX = "formo-";
2
2
 
3
3
  const generateStoragePrefix = (prefix: string) => `${STORAGE_PREFIX}${prefix}`;
4
4
 
5
- export const SESSION_IDENTIFIED_KEY =
6
- generateStoragePrefix("session-identified");
5
+ export const SESSION_WALLET_DETECTED_KEY = generateStoragePrefix(
6
+ "session-wallet-detected"
7
+ );
7
8
  export const SESSION_CURRENT_URL_KEY = generateStoragePrefix(
8
9
  "analytics-current-url"
9
10
  );
@@ -1,9 +1,10 @@
1
1
  export enum Event {
2
- PAGE = 'page_hit',
3
- IDENTIFY = 'identify',
4
- CONNECT = 'connect',
5
- DISCONNECT = 'disconnect',
6
- CHAIN_CHANGED = 'chain_changed',
7
- SIGNATURE = 'signature',
8
- TRANSACTION = 'transaction',
2
+ PAGE = "page_hit",
3
+ IDENTIFY = "identify",
4
+ DETECT_WALLET = "detect_wallet",
5
+ CONNECT = "connect",
6
+ DISCONNECT = "disconnect",
7
+ CHAIN_CHANGED = "chain_changed",
8
+ SIGNATURE = "signature",
9
+ TRANSACTION = "transaction",
9
10
  }
package/src/lib/logger.ts CHANGED
@@ -5,17 +5,39 @@ export class Logger {
5
5
  private enabledLevels: Set<LogLevel>;
6
6
  private enabled: boolean;
7
7
 
8
- private constructor(enabled: boolean = true, enabledLevels: LogLevel[] = []) {
8
+ private constructor(
9
+ enabled: boolean = false,
10
+ enabledLevels: LogLevel[] = []
11
+ ) {
9
12
  this.enabled = enabled;
10
13
  this.enabledLevels = new Set(enabledLevels);
11
14
  }
12
15
 
13
- public static getInstance(
14
- enabled: boolean = false,
15
- enabledLevels: LogLevel[] = []
16
- ): Logger {
16
+ public static init(config: {
17
+ enabled?: boolean;
18
+ enabledLevels?: LogLevel[];
19
+ }): void {
20
+ // Get or create instance
21
+ const instance = Logger.getInstance();
22
+
23
+ // Update configuration
24
+ if (config.enabled !== undefined) {
25
+ instance.setEnabled(config.enabled);
26
+ }
27
+ if (config.enabledLevels !== undefined) {
28
+ instance.setEnabledLevels(config.enabledLevels);
29
+ }
30
+ }
31
+
32
+ public static getInstance(config?: {
33
+ enabled?: boolean;
34
+ enabledLevels?: LogLevel[];
35
+ }): Logger {
17
36
  if (!Logger.instance) {
18
- Logger.instance = new Logger(enabled, enabledLevels);
37
+ Logger.instance = new Logger(
38
+ config?.enabled ?? false,
39
+ config?.enabledLevels ?? []
40
+ );
19
41
  }
20
42
  return Logger.instance;
21
43
  }
package/src/lib/queue.ts CHANGED
@@ -96,16 +96,17 @@ export class EventQueue {
96
96
  }
97
97
 
98
98
  //#region Public functions
99
- async enqueue(message: RequestEvent, callback?: (...args: any) => void) {
99
+ async enqueue(event: RequestEvent, callback?: (...args: any) => void) {
100
100
  callback = callback || noop;
101
101
 
102
- const formattedTimestamp = toDateHourMinute(new Date(message.timestamp));
103
- message.timestamp = formattedTimestamp;
102
+ const formattedTimestamp = toDateHourMinute(new Date(event.timestamp));
103
+ const originTimestamp = event.timestamp;
104
+ event.timestamp = formattedTimestamp;
104
105
 
105
- const messageString = JSON.stringify(message);
106
- const hashToken = await hash(messageString);
106
+ const eventString = JSON.stringify(event);
107
+ const eventId = await hash(eventString);
107
108
  // check if the message already exists
108
- if (await this.isDuplicated(hashToken)) {
109
+ if (await this.isDuplicate(eventId)) {
109
110
  logger.warn(
110
111
  `Event already enqueued, try again after ${millisecondsToSecond(
111
112
  this.flushIntervalMs
@@ -114,10 +115,10 @@ export class EventQueue {
114
115
  return;
115
116
  }
116
117
 
117
- this.queue.push({ message: { ...message, id: hashToken }, callback });
118
+ this.queue.push({ message: { ...event, timestamp: originTimestamp, id: eventId }, callback });
118
119
 
119
120
  logger.log(
120
- `Event enqueued: ${getActionDescriptor(message.action, message.payload)}`
121
+ `Event enqueued: ${getActionDescriptor(event.action, event.payload)}`
121
122
  );
122
123
 
123
124
  if (!this.flushed) {
@@ -220,11 +221,11 @@ export class EventQueue {
220
221
  return false;
221
222
  }
222
223
 
223
- private async isDuplicated(hashToken: string) {
224
+ private async isDuplicate(eventId: string) {
224
225
  // check if exists a message with identical payload within 1 minute
225
- if (this.payloadHashes.has(hashToken)) return true;
226
+ if (this.payloadHashes.has(eventId)) return true;
226
227
 
227
- this.payloadHashes.add(hashToken);
228
+ this.payloadHashes.add(eventId);
228
229
  return false;
229
230
  }
230
231
 
@@ -2,7 +2,7 @@ import { UUID } from "crypto";
2
2
 
3
3
  export interface RequestEvent {
4
4
  anonymous_id: UUID;
5
- user_id: UUID | null;
5
+ user_id: string | null;
6
6
  action: string;
7
7
  payload: Record<string, unknown>;
8
8
  address: string | null;