@formo/analytics 1.11.13 → 1.11.14-alpha.2

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 (37) hide show
  1. package/CONTRIBUTING.md +43 -0
  2. package/README.md +4 -251
  3. package/dist/cjs/src/FormoAnalytics.d.ts +10 -9
  4. package/dist/cjs/src/FormoAnalytics.d.ts.map +1 -1
  5. package/dist/cjs/src/FormoAnalytics.js +215 -132
  6. package/dist/cjs/src/FormoAnalytics.js.map +1 -1
  7. package/dist/cjs/src/FormoAnalyticsProvider.d.ts.map +1 -1
  8. package/dist/cjs/src/FormoAnalyticsProvider.js +53 -37
  9. package/dist/cjs/src/FormoAnalyticsProvider.js.map +1 -1
  10. package/dist/cjs/src/constants/config.d.ts +0 -1
  11. package/dist/cjs/src/constants/config.d.ts.map +1 -1
  12. package/dist/cjs/src/constants/config.js +1 -2
  13. package/dist/cjs/src/constants/config.js.map +1 -1
  14. package/dist/cjs/src/types/base.d.ts +1 -1
  15. package/dist/cjs/src/types/base.d.ts.map +1 -1
  16. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  17. package/dist/esm/src/FormoAnalytics.d.ts +10 -9
  18. package/dist/esm/src/FormoAnalytics.d.ts.map +1 -1
  19. package/dist/esm/src/FormoAnalytics.js +216 -133
  20. package/dist/esm/src/FormoAnalytics.js.map +1 -1
  21. package/dist/esm/src/FormoAnalyticsProvider.d.ts.map +1 -1
  22. package/dist/esm/src/FormoAnalyticsProvider.js +53 -37
  23. package/dist/esm/src/FormoAnalyticsProvider.js.map +1 -1
  24. package/dist/esm/src/constants/config.d.ts +0 -1
  25. package/dist/esm/src/constants/config.d.ts.map +1 -1
  26. package/dist/esm/src/constants/config.js +0 -1
  27. package/dist/esm/src/constants/config.js.map +1 -1
  28. package/dist/esm/src/types/base.d.ts +1 -1
  29. package/dist/esm/src/types/base.d.ts.map +1 -1
  30. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  31. package/dist/index.umd.min.js +1 -1
  32. package/dist/index.umd.min.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/FormoAnalytics.ts +169 -123
  35. package/src/FormoAnalyticsProvider.tsx +36 -30
  36. package/src/constants/config.ts +0 -1
  37. package/src/types/base.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formo/analytics",
3
- "version": "1.11.13",
3
+ "version": "1.11.14-alpha.2",
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.6.3",
66
+ "typescript": "^5.7.2",
67
67
  "webpack": "^5.74.0",
68
68
  "webpack-cli": "^4.10.0"
69
69
  },
@@ -2,7 +2,6 @@ import axios from "axios";
2
2
  import {
3
3
  COUNTRY_LIST,
4
4
  EVENTS_API_URL,
5
- SESSION_STORAGE_ID_KEY,
6
5
  Event,
7
6
  } from "./constants";
8
7
  import { H } from "highlight.run";
@@ -12,7 +11,7 @@ interface IFormoAnalytics {
12
11
  /**
13
12
  * Initializes the FormoAnalytics instance with the provided API key and project ID.
14
13
  */
15
- init(apiKey: string, options: Options): Promise<FormoAnalytics>;
14
+ init(apiKey: string, options?: Options): Promise<FormoAnalytics>;
16
15
 
17
16
  /**
18
17
  * Tracks page visit events.
@@ -51,7 +50,6 @@ export class FormoAnalytics implements IFormoAnalytics {
51
50
 
52
51
  private walletAddressSessionKey = "walletAddress";
53
52
  private config: Config;
54
- private sessionIdKey: string = SESSION_STORAGE_ID_KEY;
55
53
  private timezoneToCountry: Record<string, string> = COUNTRY_LIST;
56
54
 
57
55
  currentChainId?: string | null;
@@ -72,7 +70,10 @@ export class FormoAnalytics implements IFormoAnalytics {
72
70
  }
73
71
  }
74
72
 
75
- static async init(apiKey: string, options: Options): Promise<FormoAnalytics> {
73
+ static async init(
74
+ apiKey: string,
75
+ options?: Options
76
+ ): Promise<FormoAnalytics> {
76
77
  const config = {
77
78
  token: apiKey,
78
79
  };
@@ -86,64 +87,20 @@ export class FormoAnalytics implements IFormoAnalytics {
86
87
  return this._provider;
87
88
  }
88
89
 
89
- private getSessionId() {
90
- const existingSessionId = this.getCookieValue(this.sessionIdKey);
91
-
92
- if (existingSessionId) {
93
- return existingSessionId;
94
- }
95
-
96
- const newSessionId = this.generateSessionId();
97
- return newSessionId;
98
- }
99
-
100
- private getOrigin(): string {
101
- return window.location.origin || "ORIGIN_NOT_FOUND";
102
- }
103
-
104
- // Function to set the session cookie
105
- private setSessionCookie(): void {
106
- const sessionId = this.getSessionId();
107
- let cookieValue = `${
108
- this.sessionIdKey
109
- }=${sessionId}; Max-Age=1800; path=/; secure; domain=${this.getOrigin()}`;
110
- document.cookie = cookieValue;
111
- }
112
-
113
- // Function to generate a new session ID
114
- private generateSessionId(): string {
115
- return crypto.randomUUID();
116
- }
117
-
118
- // Function to get a cookie value by name
119
- private getCookieValue(name: string): string | undefined {
120
- const cookies = document.cookie.split(";").reduce((acc, cookie) => {
121
- const [key, value] = cookie.split("=");
122
- acc[key.trim()] = value;
123
- return acc;
124
- }, {} as Record<string, string>);
125
- return cookies[name];
126
- }
127
90
 
128
91
  // Function to send tracking data
129
92
  private async trackEvent(action: string, payload: any) {
130
93
  const maxRetries = 3;
131
94
  let attempt = 0;
132
95
 
133
- this.setSessionCookie();
134
96
  const address = await this.getCurrentWallet();
135
97
 
136
98
  const requestData = {
137
99
  address: address,
138
- session_id: this.getSessionId(),
139
100
  timestamp: new Date().toISOString(),
140
101
  action,
141
102
  version: "1",
142
- payload: {
143
- // common fields
144
- ...this.getCommonTrackingFields(),
145
- ...payload,
146
- },
103
+ payload: await this.buildEventPayload(payload),
147
104
  };
148
105
 
149
106
  const sendRequest = async (): Promise<void> => {
@@ -190,24 +147,71 @@ export class FormoAnalytics implements IFormoAnalytics {
190
147
  await sendRequest();
191
148
  }
192
149
 
193
- private getCommonTrackingFields() {
194
- let location: string | undefined;
195
- let language: string = "en";
150
+ // Function to track page hits
151
+ private trackPageHit() {
152
+ if (this.isAutomationEnvironment()) return;
153
+
154
+ const pathname = window.location.pathname;
155
+ const href = window.location.href;
156
+
157
+ setTimeout(async () => {
158
+ this.trackEvent(Event.PAGE, {
159
+ pathname,
160
+ href,
161
+ });
162
+ }, 300);
163
+ }
164
+
165
+ private isAutomationEnvironment(): boolean {
166
+ return (
167
+ window.__nightmare ||
168
+ window.navigator.webdriver ||
169
+ window.Cypress ||
170
+ false
171
+ );
172
+ }
173
+
174
+ private getUserLocation(): string | undefined {
196
175
  try {
197
176
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
198
- location = this.timezoneToCountry[timezone];
199
- language =
200
- navigator.languages && navigator.languages.length
177
+ return this.timezoneToCountry[timezone];
178
+ } catch (error) {
179
+ console.error("Error resolving timezone:", error);
180
+ return undefined;
181
+ }
182
+ }
183
+
184
+ private getUserLanguage(): string {
185
+ try {
186
+ return (
187
+ (navigator.languages && navigator.languages.length
201
188
  ? navigator.languages[0]
202
- : navigator.language || "en";
189
+ : navigator.language) || "en"
190
+ );
203
191
  } catch (error) {
204
- console.error("Error resolving timezone or language:", error);
192
+ console.error("Error resolving language:", error);
193
+ return "en";
205
194
  }
195
+ }
206
196
 
197
+ async buildEventPayload(
198
+ eventSpecificPayload: Record<string, unknown> = {}
199
+ ): Promise<Record<string, unknown>> {
207
200
  const url = new URL(window.location.href);
208
201
  const params = new URLSearchParams(url.search);
202
+
203
+ const location = this.getUserLocation();
204
+ const language = this.getUserLanguage();
205
+
206
+ const address = await this.getAndStoreConnectedAddress();
207
+ if (address === null) {
208
+ console.warn("Wallet address could not be retrieved.");
209
+ }
210
+
211
+ // common fields
209
212
  return {
210
213
  "user-agent": window.navigator.userAgent,
214
+ address,
211
215
  locale: language,
212
216
  location,
213
217
  referrer: document.referrer,
@@ -215,24 +219,13 @@ export class FormoAnalytics implements IFormoAnalytics {
215
219
  utm_medium: params.get("utm_medium"),
216
220
  utm_campaign: params.get("utm_campaign"),
217
221
  ref: params.get("ref"),
222
+ ...eventSpecificPayload,
218
223
  };
219
224
  }
220
225
 
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);
232
- }
233
-
234
226
  private trackProvider(provider: EIP1193Provider) {
235
227
  if (provider === this._provider) {
228
+ console.log("Provider already tracked.");
236
229
  return;
237
230
  }
238
231
 
@@ -250,6 +243,7 @@ export class FormoAnalytics implements IFormoAnalytics {
250
243
  }
251
244
  }
252
245
 
246
+ console.log("Tracking new provider:", provider);
253
247
  this._provider = provider;
254
248
 
255
249
  this.getCurrentWallet();
@@ -257,14 +251,33 @@ export class FormoAnalytics implements IFormoAnalytics {
257
251
  this.registerChainChangedListener();
258
252
  }
259
253
 
254
+ private async getAndStoreConnectedAddress(): Promise<string | null> {
255
+ console.log(
256
+ "Session data missing. Attempting to fetch address from provider."
257
+ );
258
+ try {
259
+ const accounts = await this.fetchAccounts();
260
+ if (accounts && accounts.length > 0) {
261
+ const address = accounts[0];
262
+ this.storeWalletAddress(address);
263
+ return address;
264
+ }
265
+ } catch (err) {
266
+ console.log("Failed to fetch accounts from provider:", err);
267
+ }
268
+ return null;
269
+ }
270
+
260
271
  private async getCurrentWallet() {
261
272
  if (!this.provider) {
262
273
  console.warn("FormoAnalytics::getCurrentWallet: the provider is not set");
263
274
  return;
264
275
  }
276
+
265
277
  const sessionData = sessionStorage.getItem(this.walletAddressSessionKey);
278
+
266
279
  if (!sessionData) {
267
- return null;
280
+ return await this.getAndStoreConnectedAddress();
268
281
  }
269
282
 
270
283
  const parsedData = JSON.parse(sessionData);
@@ -272,7 +285,7 @@ export class FormoAnalytics implements IFormoAnalytics {
272
285
  const currentTime = Date.now();
273
286
 
274
287
  if (currentTime - parsedData.timestamp > sessionExpiry) {
275
- console.warn("Session expired. Ignoring wallet address.");
288
+ console.log("Session expired. Ignoring wallet address.");
276
289
  sessionStorage.removeItem(this.walletAddressSessionKey); // Clear expired session data
277
290
  return "";
278
291
  }
@@ -281,16 +294,61 @@ export class FormoAnalytics implements IFormoAnalytics {
281
294
  return parsedData.address || "";
282
295
  }
283
296
 
297
+ // Utility to fetch accounts
298
+ private async fetchAccounts(): Promise<string[] | null> {
299
+ try {
300
+ const res: string[] | null | undefined = await this.provider?.request({
301
+ method: "eth_accounts",
302
+ });
303
+ if (!res || res.length === 0) {
304
+ console.log(
305
+ "FormoAnalytics::fetchAccounts: unable to get account. eth_accounts returned empty"
306
+ );
307
+ return null;
308
+ }
309
+ return res;
310
+ } catch (err) {
311
+ if ((err as any).code !== 4001) {
312
+ console.log(
313
+ "FormoAnalytics::fetchAccounts: eth_accounts threw an error",
314
+ err
315
+ );
316
+ }
317
+ return null;
318
+ }
319
+ }
320
+
321
+ // Utility to fetch chain ID
322
+ private async fetchChainId(): Promise<string | null> {
323
+ try {
324
+ const chainIdHex = await this.provider?.request<string>({
325
+ method: "eth_chainId",
326
+ });
327
+ if (!chainIdHex) {
328
+ console.log(
329
+ "FormoAnalytics::fetchChainId: chainIdHex is null or undefined"
330
+ );
331
+ return null;
332
+ }
333
+ return chainIdHex;
334
+ } catch (err) {
335
+ console.log(
336
+ "FormoAnalytics::fetchChainId: eth_chainId threw an error",
337
+ err
338
+ );
339
+ return null;
340
+ }
341
+ }
342
+
284
343
  private async getCurrentChainId(): Promise<string> {
285
344
  if (!this.provider) {
286
- console.error("FormoAnalytics::getCurrentChainId: provider not set");
345
+ console.log("FormoAnalytics::getCurrentChainId: provider not set");
287
346
  }
288
- const chainIdHex = await this.provider?.request<string>({
289
- method: "eth_chainId",
290
- });
347
+
348
+ const chainIdHex = await this.fetchChainId();
291
349
  // Because we're connected, the chainId cannot be null
292
350
  if (!chainIdHex) {
293
- console.error(
351
+ console.log(
294
352
  `FormoAnalytics::getCurrentChainId: chainIdHex is: ${chainIdHex}`
295
353
  );
296
354
  }
@@ -342,64 +400,60 @@ export class FormoAnalytics implements IFormoAnalytics {
342
400
  this.storeWalletAddress(address);
343
401
  }
344
402
 
345
- private onAddressDisconnected() {
346
- if (!this.currentConnectedAddress) {
403
+ private handleDisconnection(chainId?: string, address?: string) {
404
+ if (!address) {
347
405
  return;
348
406
  }
349
-
350
407
  const payload = {
351
- chain_id: this.currentChainId,
352
- address: this.currentConnectedAddress,
408
+ chain_id: chainId || this.currentChainId,
409
+ address,
353
410
  };
354
411
  this.currentChainId = undefined;
355
412
  this.currentConnectedAddress = undefined;
356
413
  this.clearWalletAddress();
357
-
358
414
  return this.trackEvent(Event.DISCONNECT, payload);
359
415
  }
360
416
 
417
+ private onAddressDisconnected() {
418
+ if (!this.currentConnectedAddress) {
419
+ return;
420
+ }
421
+ this.handleDisconnection(this.currentChainId || undefined, this.currentConnectedAddress);
422
+ }
423
+
361
424
  private async onChainChanged(chainIdHex: string) {
362
425
  this.currentChainId = parseInt(chainIdHex).toString();
363
426
  if (!this.currentConnectedAddress) {
364
427
  if (!this.provider) {
365
- console.error(
366
- "error",
428
+ console.log(
367
429
  "FormoAnalytics::onChainChanged: provider not found. CHAIN_CHANGED not reported"
368
430
  );
369
431
  return;
370
432
  }
371
433
 
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
- }
383
-
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
- }
434
+ // Attempt to fetch and store the connected address
435
+ const address = await this.getAndStoreConnectedAddress();
436
+ if (!address) {
437
+ console.log(
438
+ "FormoAnalytics::onChainChanged: Unable to fetch or store connected address"
439
+ );
440
+ return;
396
441
  }
442
+
443
+ this.currentConnectedAddress = address[0];
397
444
  }
398
445
 
399
- return this.chain({
400
- chainId: this.currentChainId,
401
- address: this.currentConnectedAddress,
402
- });
446
+ // Proceed only if the address exists
447
+ if (this.currentConnectedAddress) {
448
+ return this.chain({
449
+ chainId: this.currentChainId,
450
+ address: this.currentConnectedAddress,
451
+ });
452
+ } else {
453
+ console.log(
454
+ "FormoAnalytics::onChainChanged: currentConnectedAddress is null despite fetch attempt"
455
+ );
456
+ }
403
457
  }
404
458
 
405
459
  /**
@@ -408,7 +462,7 @@ export class FormoAnalytics implements IFormoAnalytics {
408
462
  */
409
463
  private storeWalletAddress(address: string): void {
410
464
  if (!address) {
411
- console.error("No wallet address provided to store.");
465
+ console.log("No wallet address provided to store.");
412
466
  return;
413
467
  }
414
468
 
@@ -459,15 +513,7 @@ export class FormoAnalytics implements IFormoAnalytics {
459
513
  // `disconnect` detection
460
514
  return;
461
515
  }
462
-
463
- const payload = {
464
- chain_id: params?.chainId || this.currentChainId,
465
- address,
466
- };
467
- this.currentChainId = undefined;
468
- this.currentConnectedAddress = undefined;
469
-
470
- return this.trackEvent(Event.DISCONNECT, payload);
516
+ return this.handleDisconnection((params?.chainId?.toString() || this.currentChainId) as string, address);
471
517
  }
472
518
 
473
519
  chain({ chainId, address }: { chainId: ChainID; address?: string }) {
@@ -26,6 +26,40 @@ export const FormoAnalyticsProvider = ({
26
26
  const [isInitialized, setIsInitialized] = useState(false);
27
27
  const initializedStartedRef = useRef(false);
28
28
 
29
+ const initializeHighlight = async () => {
30
+ if (HIGHLIGHT_PROJECT_ID) {
31
+ try {
32
+ H.init(HIGHLIGHT_PROJECT_ID, {
33
+ serviceName: 'formo-analytics-sdk',
34
+ tracingOrigins: true,
35
+ networkRecording: {
36
+ enabled: true,
37
+ recordHeadersAndBody: true,
38
+ urlBlocklist: [
39
+ 'https://www.googleapis.com/identitytoolkit',
40
+ 'https://securetoken.googleapis.com',
41
+ ],
42
+ },
43
+ });
44
+ console.log('Highlight.run initialized successfully');
45
+ } catch (error) {
46
+ console.error('Failed to initialize Highlight.run', error);
47
+ }
48
+ }
49
+ };
50
+
51
+ const initializeFormoAnalytics = async (apiKey: string, options: any) => {
52
+ try {
53
+ const sdkInstance = await FormoAnalytics.init(apiKey, options);
54
+ setSdk(sdkInstance);
55
+ console.log('FormoAnalytics SDK initialized successfully');
56
+ } catch (error) {
57
+ console.error('Failed to initialize FormoAnalytics SDK', error);
58
+ } finally {
59
+ setIsInitialized(true); // Ensure UI renders even after failure
60
+ }
61
+ };
62
+
29
63
  useEffect(() => {
30
64
  const initialize = async () => {
31
65
  if (!apiKey) {
@@ -38,37 +72,9 @@ export const FormoAnalyticsProvider = ({
38
72
  }
39
73
  if (initializedStartedRef.current) return;
40
74
  initializedStartedRef.current = true;
41
- // Initialize Highlight.run if project ID is available
42
- if (HIGHLIGHT_PROJECT_ID) {
43
- try {
44
- H.init(HIGHLIGHT_PROJECT_ID, {
45
- serviceName: 'formo-analytics-sdk',
46
- tracingOrigins: true,
47
- networkRecording: {
48
- enabled: true,
49
- recordHeadersAndBody: true,
50
- urlBlocklist: [
51
- 'https://www.googleapis.com/identitytoolkit',
52
- 'https://securetoken.googleapis.com',
53
- ],
54
- },
55
- });
56
- console.log('Highlight.run initialized successfully');
57
- } catch (error) {
58
- console.error('Failed to initialize Highlight.run', error);
59
- }
60
- }
61
75
 
62
- // Initialize FormoAnalytics
63
- try {
64
- const sdkInstance = await FormoAnalytics.init(apiKey, options);
65
- setSdk(sdkInstance);
66
- console.log('FormoAnalytics SDK initialized successfully');
67
- } catch (error) {
68
- console.error('Failed to initialize FormoAnalytics SDK', error);
69
- } finally {
70
- setIsInitialized(true); // Ensure UI renders even after failure
71
- }
76
+ await initializeHighlight();
77
+ await initializeFormoAnalytics(apiKey, options);
72
78
  };
73
79
 
74
80
  initialize();
@@ -1,5 +1,4 @@
1
1
  export const EVENTS_API_URL = 'https://events.formo.so/events';
2
- export const SESSION_STORAGE_ID_KEY = 'formo-analytics-session-id';
3
2
  export const COUNTRY_LIST = {
4
3
  // Africa
5
4
  'Africa/Accra': 'GH',
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
  }