@formo/analytics 1.11.11 → 1.11.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formo/analytics",
3
- "version": "1.11.11",
3
+ "version": "1.11.13",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/getformo/sdk.git"
@@ -63,7 +63,7 @@
63
63
  "sinon-chai": "^3.7.0",
64
64
  "ts-loader": "^9.3.1",
65
65
  "ts-node": "^10.8.2",
66
- "typescript": "^5.7.2",
66
+ "typescript": "^5.6.3",
67
67
  "webpack": "^5.74.0",
68
68
  "webpack-cli": "^4.10.0"
69
69
  },
@@ -1,18 +1,18 @@
1
- import axios from 'axios';
1
+ import axios from "axios";
2
2
  import {
3
3
  COUNTRY_LIST,
4
4
  EVENTS_API_URL,
5
5
  SESSION_STORAGE_ID_KEY,
6
6
  Event,
7
- } from './constants';
8
- import { H } from 'highlight.run';
9
- import { ChainID, EIP1193Provider, Options } from './types';
7
+ } from "./constants";
8
+ import { H } from "highlight.run";
9
+ import { ChainID, EIP1193Provider, Options } from "./types";
10
10
 
11
11
  interface IFormoAnalytics {
12
12
  /**
13
13
  * Initializes the FormoAnalytics instance with the provided API key and project ID.
14
14
  */
15
- init(apiKey: string, options?: Options): Promise<FormoAnalytics>;
15
+ init(apiKey: string, options: Options): Promise<FormoAnalytics>;
16
16
 
17
17
  /**
18
18
  * Tracks page visit events.
@@ -49,7 +49,7 @@ export class FormoAnalytics implements IFormoAnalytics {
49
49
  (...args: unknown[]) => void
50
50
  > = {};
51
51
 
52
- private walletAddressSessionKey = 'walletAddress';
52
+ private walletAddressSessionKey = "walletAddress";
53
53
  private config: Config;
54
54
  private sessionIdKey: string = SESSION_STORAGE_ID_KEY;
55
55
  private timezoneToCountry: Record<string, string> = COUNTRY_LIST;
@@ -72,10 +72,7 @@ export class FormoAnalytics implements IFormoAnalytics {
72
72
  }
73
73
  }
74
74
 
75
- static async init(
76
- apiKey: string,
77
- options?: Options
78
- ): Promise<FormoAnalytics> {
75
+ static async init(apiKey: string, options: Options): Promise<FormoAnalytics> {
79
76
  const config = {
80
77
  token: apiKey,
81
78
  };
@@ -101,7 +98,7 @@ export class FormoAnalytics implements IFormoAnalytics {
101
98
  }
102
99
 
103
100
  private getOrigin(): string {
104
- return window.location.origin || 'ORIGIN_NOT_FOUND';
101
+ return window.location.origin || "ORIGIN_NOT_FOUND";
105
102
  }
106
103
 
107
104
  // Function to set the session cookie
@@ -120,8 +117,8 @@ export class FormoAnalytics implements IFormoAnalytics {
120
117
 
121
118
  // Function to get a cookie value by name
122
119
  private getCookieValue(name: string): string | undefined {
123
- const cookies = document.cookie.split(';').reduce((acc, cookie) => {
124
- const [key, value] = cookie.split('=');
120
+ const cookies = document.cookie.split(";").reduce((acc, cookie) => {
121
+ const [key, value] = cookie.split("=");
125
122
  acc[key.trim()] = value;
126
123
  return acc;
127
124
  }, {} as Record<string, string>);
@@ -141,8 +138,12 @@ export class FormoAnalytics implements IFormoAnalytics {
141
138
  session_id: this.getSessionId(),
142
139
  timestamp: new Date().toISOString(),
143
140
  action,
144
- version: '1',
145
- payload,
141
+ version: "1",
142
+ payload: {
143
+ // common fields
144
+ ...this.getCommonTrackingFields(),
145
+ ...payload,
146
+ },
146
147
  };
147
148
 
148
149
  const sendRequest = async (): Promise<void> => {
@@ -152,14 +153,14 @@ export class FormoAnalytics implements IFormoAnalytics {
152
153
  JSON.stringify(requestData),
153
154
  {
154
155
  headers: {
155
- 'Content-Type': 'application/json',
156
+ "Content-Type": "application/json",
156
157
  Authorization: `Bearer ${this.apiKey}`,
157
158
  },
158
159
  }
159
160
  );
160
161
 
161
162
  if (response.status >= 200 && response.status < 300) {
162
- console.log('Event sent successfully:', action);
163
+ console.log("Event sent successfully:", action);
163
164
  } else {
164
165
  throw new Error(`Failed with status: ${response.status}`);
165
166
  }
@@ -189,83 +190,49 @@ export class FormoAnalytics implements IFormoAnalytics {
189
190
  await sendRequest();
190
191
  }
191
192
 
192
- // Function to track page hits
193
- private trackPageHit() {
194
- if (this.isAutomationEnvironment()) return;
195
-
196
- const location = this.getUserLocation();
197
- const language = this.getUserLanguage();
198
-
199
- setTimeout(async () => {
200
- const eventData = await this.buildPageEventData(location, language);
201
- this.trackEvent(Event.PAGE, eventData);
202
- }, 300);
203
- }
204
-
205
- private isAutomationEnvironment(): boolean {
206
- return (
207
- window.__nightmare ||
208
- window.navigator.webdriver ||
209
- window.Cypress ||
210
- false
211
- );
212
- }
213
-
214
- private getUserLocation(): string | undefined {
193
+ private getCommonTrackingFields() {
194
+ let location: string | undefined;
195
+ let language: string = "en";
215
196
  try {
216
197
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
217
- return this.timezoneToCountry[timezone];
218
- } catch (error) {
219
- console.error('Error resolving timezone:', error);
220
- return undefined;
221
- }
222
- }
223
-
224
- private getUserLanguage(): string {
225
- try {
226
- return (
227
- (navigator.languages && navigator.languages.length
198
+ location = this.timezoneToCountry[timezone];
199
+ language =
200
+ navigator.languages && navigator.languages.length
228
201
  ? navigator.languages[0]
229
- : navigator.language) || 'en'
230
- );
202
+ : navigator.language || "en";
231
203
  } catch (error) {
232
- console.error('Error resolving language:', error);
233
- return 'en';
204
+ console.error("Error resolving timezone or language:", error);
234
205
  }
235
- }
236
206
 
237
- async buildPageEventData(location: string | undefined, language: string): Promise<Record<string, unknown>> {
238
207
  const url = new URL(window.location.href);
239
208
  const params = new URLSearchParams(url.search);
240
-
241
- const address = await this.getAndStoreConnectedAddress();
242
- if (address === null) {
243
- console.warn('Wallet address could not be retrieved.');
244
- }
245
-
246
- const eventData: Record<string, unknown> = {
247
- 'user-agent': window.navigator.userAgent,
209
+ return {
210
+ "user-agent": window.navigator.userAgent,
248
211
  locale: language,
249
212
  location,
250
213
  referrer: document.referrer,
251
- pathname: window.location.pathname,
252
- href: window.location.href,
253
- utm_source: params.get('utm_source'),
254
- utm_medium: params.get('utm_medium'),
255
- utm_campaign: params.get('utm_campaign'),
256
- ref: params.get('ref'),
214
+ utm_source: params.get("utm_source"),
215
+ utm_medium: params.get("utm_medium"),
216
+ utm_campaign: params.get("utm_campaign"),
217
+ ref: params.get("ref"),
257
218
  };
258
-
259
- if (address !== null) {
260
- eventData['address'] = address;
261
- }
262
-
263
- return eventData;
219
+ }
220
+
221
+ // Function to track page hits
222
+ private trackPageHit() {
223
+ if (window.__nightmare || window.navigator.webdriver || window.Cypress)
224
+ return;
225
+
226
+ setTimeout(() => {
227
+ this.trackEvent(Event.PAGE, {
228
+ pathname: window.location.pathname,
229
+ href: window.location.href,
230
+ });
231
+ }, 300);
264
232
  }
265
233
 
266
234
  private trackProvider(provider: EIP1193Provider) {
267
235
  if (provider === this._provider) {
268
- console.log('Provider already tracked.');
269
236
  return;
270
237
  }
271
238
 
@@ -283,7 +250,6 @@ export class FormoAnalytics implements IFormoAnalytics {
283
250
  }
284
251
  }
285
252
 
286
- console.log('Tracking new provider:', provider);
287
253
  this._provider = provider;
288
254
 
289
255
  this.getCurrentWallet();
@@ -291,33 +257,14 @@ export class FormoAnalytics implements IFormoAnalytics {
291
257
  this.registerChainChangedListener();
292
258
  }
293
259
 
294
- private async getAndStoreConnectedAddress(): Promise<string | null> {
295
- console.warn(
296
- 'Session data missing. Attempting to fetch address from provider.'
297
- );
298
- try {
299
- const accounts = await this.fetchAccounts();
300
- if (accounts && accounts.length > 0) {
301
- const address = accounts[0];
302
- this.storeWalletAddress(address);
303
- return address;
304
- }
305
- } catch (err) {
306
- console.error('Failed to fetch accounts from provider:', err);
307
- }
308
- return null;
309
- }
310
-
311
260
  private async getCurrentWallet() {
312
261
  if (!this.provider) {
313
- console.warn('FormoAnalytics::getCurrentWallet: the provider is not set');
262
+ console.warn("FormoAnalytics::getCurrentWallet: the provider is not set");
314
263
  return;
315
264
  }
316
-
317
265
  const sessionData = sessionStorage.getItem(this.walletAddressSessionKey);
318
-
319
266
  if (!sessionData) {
320
- return await this.getAndStoreConnectedAddress();
267
+ return null;
321
268
  }
322
269
 
323
270
  const parsedData = JSON.parse(sessionData);
@@ -325,70 +272,22 @@ export class FormoAnalytics implements IFormoAnalytics {
325
272
  const currentTime = Date.now();
326
273
 
327
274
  if (currentTime - parsedData.timestamp > sessionExpiry) {
328
- console.warn('Session expired. Ignoring wallet address.');
275
+ console.warn("Session expired. Ignoring wallet address.");
329
276
  sessionStorage.removeItem(this.walletAddressSessionKey); // Clear expired session data
330
- return '';
277
+ return "";
331
278
  }
332
279
 
333
280
  this.onAddressConnected(parsedData.address);
334
- return parsedData.address || '';
335
- }
336
-
337
- // Utility to fetch accounts
338
- private async fetchAccounts(): Promise<string[] | null> {
339
- try {
340
- const res: string[] | null | undefined = await this.provider?.request({
341
- method: 'eth_accounts',
342
- });
343
- if (!res || res.length === 0) {
344
- console.error(
345
- 'error',
346
- 'FormoAnalytics::fetchAccounts: unable to get account. eth_accounts returned empty'
347
- );
348
- return null;
349
- }
350
- return res;
351
- } catch (err) {
352
- if ((err as any).code !== 4001) {
353
- console.error(
354
- 'error',
355
- 'FormoAnalytics::fetchAccounts: eth_accounts threw an error',
356
- err
357
- );
358
- }
359
- return null;
360
- }
361
- }
362
-
363
- // Utility to fetch chain ID
364
- private async fetchChainId(): Promise<string | null> {
365
- try {
366
- const chainIdHex = await this.provider?.request<string>({
367
- method: 'eth_chainId',
368
- });
369
- if (!chainIdHex) {
370
- console.error(
371
- 'FormoAnalytics::fetchChainId: chainIdHex is null or undefined'
372
- );
373
- return null;
374
- }
375
- return chainIdHex;
376
- } catch (err) {
377
- console.error(
378
- 'error',
379
- 'FormoAnalytics::fetchChainId: eth_chainId threw an error',
380
- err
381
- );
382
- return null;
383
- }
281
+ return parsedData.address || "";
384
282
  }
385
283
 
386
284
  private async getCurrentChainId(): Promise<string> {
387
285
  if (!this.provider) {
388
- console.error('FormoAnalytics::getCurrentChainId: provider not set');
286
+ console.error("FormoAnalytics::getCurrentChainId: provider not set");
389
287
  }
390
-
391
- const chainIdHex = await this.fetchChainId();
288
+ const chainIdHex = await this.provider?.request<string>({
289
+ method: "eth_chainId",
290
+ });
392
291
  // Because we're connected, the chainId cannot be null
393
292
  if (!chainIdHex) {
394
293
  console.error(
@@ -403,19 +302,19 @@ export class FormoAnalytics implements IFormoAnalytics {
403
302
  const listener = (...args: unknown[]) =>
404
303
  this.onAddressChanged(args[0] as string[]);
405
304
 
406
- this._provider?.on('accountsChanged', listener);
407
- this._registeredProviderListeners['accountsChanged'] = listener;
305
+ this._provider?.on("accountsChanged", listener);
306
+ this._registeredProviderListeners["accountsChanged"] = listener;
408
307
 
409
308
  const onAddressDisconnected = this.onAddressDisconnected.bind(this);
410
- this._provider?.on('disconnect', onAddressDisconnected);
411
- this._registeredProviderListeners['disconnect'] = onAddressDisconnected;
309
+ this._provider?.on("disconnect", onAddressDisconnected);
310
+ this._registeredProviderListeners["disconnect"] = onAddressDisconnected;
412
311
  }
413
312
 
414
313
  private registerChainChangedListener() {
415
314
  const listener = (...args: unknown[]) =>
416
315
  this.onChainChanged(args[0] as string);
417
- this.provider?.on('chainChanged', listener);
418
- this._registeredProviderListeners['chainChanged'] = listener;
316
+ this.provider?.on("chainChanged", listener);
317
+ this._registeredProviderListeners["chainChanged"] = listener;
419
318
  }
420
319
 
421
320
  private async onAddressChanged(addresses: string[]) {
@@ -450,7 +349,7 @@ export class FormoAnalytics implements IFormoAnalytics {
450
349
 
451
350
  const payload = {
452
351
  chain_id: this.currentChainId,
453
- address: this.getAndStoreConnectedAddress(),
352
+ address: this.currentConnectedAddress,
454
353
  };
455
354
  this.currentChainId = undefined;
456
355
  this.currentConnectedAddress = undefined;
@@ -464,37 +363,43 @@ export class FormoAnalytics implements IFormoAnalytics {
464
363
  if (!this.currentConnectedAddress) {
465
364
  if (!this.provider) {
466
365
  console.error(
467
- 'error',
468
- 'FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported'
366
+ "error",
367
+ "FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported"
469
368
  );
470
369
  return;
471
370
  }
472
371
 
473
- // Attempt to fetch and store the connected address
474
- const address = await this.getAndStoreConnectedAddress();
475
- if (!address) {
476
- console.error(
477
- 'error',
478
- 'FormoAnalytics::onChainChanged: Unable to fetch or store connected address'
479
- );
480
- return;
481
- }
372
+ try {
373
+ const res: string[] | null | undefined = await this.provider.request({
374
+ method: "eth_accounts",
375
+ });
376
+ if (!res || res.length === 0) {
377
+ console.error(
378
+ "error",
379
+ "FormoAnalytics::onChainChanged: unable to get account. eth_accounts returned empty"
380
+ );
381
+ return;
382
+ }
482
383
 
483
- this.currentConnectedAddress = address[0];
384
+ this.currentConnectedAddress = res[0];
385
+ } catch (err) {
386
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
387
+ if ((err as any).code !== 4001) {
388
+ // 4001: The request is rejected by the user , see https://docs.metamask.io/wallet/reference/provider-api/#errors
389
+ console.error(
390
+ "error",
391
+ `FormoAnalytics::onChainChanged: unable to get account. eth_accounts threw an error`,
392
+ err
393
+ );
394
+ return;
395
+ }
396
+ }
484
397
  }
485
398
 
486
- // Proceed only if the address exists
487
- if (this.currentConnectedAddress) {
488
- return this.chain({
489
- chainId: this.currentChainId,
490
- address: this.currentConnectedAddress,
491
- });
492
- } else {
493
- console.error(
494
- 'error',
495
- 'FormoAnalytics::onChainChanged: currentConnectedAddress is null despite fetch attempt'
496
- );
497
- }
399
+ return this.chain({
400
+ chainId: this.currentChainId,
401
+ address: this.currentConnectedAddress,
402
+ });
498
403
  }
499
404
 
500
405
  /**
@@ -503,7 +408,7 @@ export class FormoAnalytics implements IFormoAnalytics {
503
408
  */
504
409
  private storeWalletAddress(address: string): void {
505
410
  if (!address) {
506
- console.error('No wallet address provided to store.');
411
+ console.error("No wallet address provided to store.");
507
412
  return;
508
413
  }
509
414
 
@@ -532,10 +437,10 @@ export class FormoAnalytics implements IFormoAnalytics {
532
437
 
533
438
  connect({ chainId, address }: { chainId: ChainID; address: string }) {
534
439
  if (!chainId) {
535
- throw new Error('FormoAnalytics::connect: chain ID cannot be empty');
440
+ throw new Error("FormoAnalytics::connect: chain ID cannot be empty");
536
441
  }
537
442
  if (!address) {
538
- throw new Error('FormoAnalytics::connect: address cannot be empty');
443
+ throw new Error("FormoAnalytics::connect: address cannot be empty");
539
444
  }
540
445
 
541
446
  this.currentChainId = chainId.toString();
@@ -567,16 +472,16 @@ export class FormoAnalytics implements IFormoAnalytics {
567
472
 
568
473
  chain({ chainId, address }: { chainId: ChainID; address?: string }) {
569
474
  if (!chainId || Number(chainId) === 0) {
570
- throw new Error('FormoAnalytics::chain: chainId cannot be empty or 0');
475
+ throw new Error("FormoAnalytics::chain: chainId cannot be empty or 0");
571
476
  }
572
477
  if (!address && !this.currentConnectedAddress) {
573
478
  throw new Error(
574
- 'FormoAnalytics::chain: address was empty and no previous address has been recorded. You can either pass an address or call connect() first'
479
+ "FormoAnalytics::chain: address was empty and no previous address has been recorded. You can either pass an address or call connect() first"
575
480
  );
576
481
  }
577
482
  if (isNaN(Number(chainId))) {
578
483
  throw new Error(
579
- 'FormoAnalytics::chain: chainId must be a valid hex or decimal number'
484
+ "FormoAnalytics::chain: chainId must be a valid hex or decimal number"
580
485
  );
581
486
  }
582
487
 
package/src/types/base.ts CHANGED
@@ -8,7 +8,7 @@ export interface Options {
8
8
 
9
9
  export interface FormoAnalyticsProviderProps {
10
10
  apiKey: string;
11
- options?: Options;
11
+ options: Options;
12
12
  disabled?: boolean;
13
13
  children: React.ReactNode;
14
14
  }