@formo/analytics 1.17.3 → 1.17.4

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 (73) hide show
  1. package/package.json +4 -1
  2. package/.env.example +0 -1
  3. package/.github/workflows/ci.yml +0 -50
  4. package/.github/workflows/release.yml +0 -54
  5. package/.husky/post-commit +0 -7
  6. package/.husky/pre-commit +0 -11
  7. package/.releaserc.js +0 -35
  8. package/CONTRIBUTING.md +0 -83
  9. package/scripts/generate-sri.sh +0 -52
  10. package/src/FormoAnalytics.ts +0 -1031
  11. package/src/FormoAnalyticsProvider.tsx +0 -84
  12. package/src/constants/base.ts +0 -6
  13. package/src/constants/config.ts +0 -660
  14. package/src/constants/events.ts +0 -21
  15. package/src/constants/index.ts +0 -3
  16. package/src/global.d.ts +0 -12
  17. package/src/index.ts +0 -3
  18. package/src/lib/event/EventFactory.ts +0 -519
  19. package/src/lib/event/EventManager.ts +0 -39
  20. package/src/lib/event/constants.ts +0 -4
  21. package/src/lib/event/index.ts +0 -3
  22. package/src/lib/event/type.ts +0 -9
  23. package/src/lib/event/utils.ts +0 -33
  24. package/src/lib/fetch.ts +0 -3
  25. package/src/lib/index.ts +0 -5
  26. package/src/lib/logger/Logger.ts +0 -115
  27. package/src/lib/logger/index.ts +0 -2
  28. package/src/lib/logger/type.ts +0 -14
  29. package/src/lib/queue/EventQueue.ts +0 -306
  30. package/src/lib/queue/index.ts +0 -2
  31. package/src/lib/queue/type.ts +0 -6
  32. package/src/lib/ramda/internal/_curry1.ts +0 -19
  33. package/src/lib/ramda/internal/_curry2.ts +0 -37
  34. package/src/lib/ramda/internal/_curry3.ts +0 -68
  35. package/src/lib/ramda/internal/_has.ts +0 -3
  36. package/src/lib/ramda/internal/_isObject.ts +0 -3
  37. package/src/lib/ramda/internal/_isPlaceholder.ts +0 -5
  38. package/src/lib/ramda/mergeDeepRight.ts +0 -13
  39. package/src/lib/ramda/mergeDeepWithKey.ts +0 -22
  40. package/src/lib/ramda/mergeWithKey.ts +0 -28
  41. package/src/lib/storage/StorageManager.ts +0 -51
  42. package/src/lib/storage/built-in/blueprint.ts +0 -17
  43. package/src/lib/storage/built-in/cookie.ts +0 -60
  44. package/src/lib/storage/built-in/memory.ts +0 -23
  45. package/src/lib/storage/built-in/web.ts +0 -57
  46. package/src/lib/storage/constant.ts +0 -2
  47. package/src/lib/storage/index.ts +0 -25
  48. package/src/lib/storage/type.ts +0 -21
  49. package/src/lib/version.ts +0 -2
  50. package/src/types/base.ts +0 -120
  51. package/src/types/events.ts +0 -126
  52. package/src/types/index.ts +0 -3
  53. package/src/types/provider.ts +0 -17
  54. package/src/utils/address.ts +0 -43
  55. package/src/utils/base.ts +0 -3
  56. package/src/utils/converter.ts +0 -44
  57. package/src/utils/generate.ts +0 -16
  58. package/src/utils/index.ts +0 -4
  59. package/src/utils/timestamp.ts +0 -9
  60. package/src/validators/address.ts +0 -69
  61. package/src/validators/agent.ts +0 -4
  62. package/src/validators/checks.ts +0 -160
  63. package/src/validators/index.ts +0 -7
  64. package/src/validators/network.ts +0 -34
  65. package/src/validators/object.ts +0 -4
  66. package/src/validators/string.ts +0 -4
  67. package/src/validators/uint8array.ts +0 -17
  68. package/test/lib/events.spec.ts +0 -12
  69. package/test/utils/address.spec.ts +0 -14
  70. package/test/utils/converter.spec.ts +0 -31
  71. package/test/validators/address.spec.ts +0 -15
  72. package/tsconfig.json +0 -28
  73. package/webpack.config.ts +0 -23
@@ -1,1031 +0,0 @@
1
- import { createStore, EIP6963ProviderDetail } from "mipd";
2
- import {
3
- EVENTS_API_URL,
4
- EventType,
5
- LOCAL_ANONYMOUS_ID_KEY,
6
- SESSION_CURRENT_URL_KEY,
7
- SESSION_USER_ID_KEY,
8
- SESSION_WALLET_DETECTED_KEY,
9
- TEventType,
10
- } from "./constants";
11
- import {
12
- cookie,
13
- EventManager,
14
- EventQueue,
15
- IEventManager,
16
- initStorageManager,
17
- logger,
18
- Logger,
19
- } from "./lib";
20
- import {
21
- Address,
22
- ChainID,
23
- Config,
24
- EIP1193Provider,
25
- IFormoAnalytics,
26
- IFormoEventContext,
27
- IFormoEventProperties,
28
- Options,
29
- RequestArguments,
30
- RPCError,
31
- SignatureStatus,
32
- TransactionStatus,
33
- } from "./types";
34
- import { isAddress, isLocalhost } from "./validators";
35
-
36
- export class FormoAnalytics implements IFormoAnalytics {
37
- private _provider?: EIP1193Provider;
38
- private _providerListeners: Record<string, (...args: unknown[]) => void> = {};
39
- private session: FormoAnalyticsSession;
40
- private eventManager: IEventManager;
41
- private _providers: readonly EIP6963ProviderDetail[] = [];
42
-
43
- config: Config;
44
- currentChainId?: ChainID;
45
- currentAddress?: Address = "";
46
- currentUserId?: string = "";
47
-
48
- private constructor(
49
- public readonly writeKey: string,
50
- public options: Options = {}
51
- ) {
52
- this.config = {
53
- writeKey,
54
- trackLocalhost: options.trackLocalhost || false,
55
- };
56
-
57
- this.session = new FormoAnalyticsSession();
58
- this.currentUserId =
59
- (cookie().get(SESSION_USER_ID_KEY) as string) || undefined;
60
-
61
- this.identify = this.identify.bind(this);
62
- this.connect = this.connect.bind(this);
63
- this.disconnect = this.disconnect.bind(this);
64
- this.chain = this.chain.bind(this);
65
- this.signature = this.signature.bind(this);
66
- this.transaction = this.transaction.bind(this);
67
- this.detect = this.detect.bind(this);
68
- this.track = this.track.bind(this);
69
-
70
- // Initialize logger with configuration from options
71
- Logger.init({
72
- enabled: options.logger?.enabled || false,
73
- enabledLevels: options.logger?.levels || [],
74
- });
75
-
76
- this.eventManager = new EventManager(
77
- new EventQueue(this.config.writeKey, {
78
- url: EVENTS_API_URL,
79
- flushAt: options.flushAt,
80
- retryCount: options.retryCount,
81
- maxQueueSize: options.maxQueueSize,
82
- flushInterval: options.flushInterval,
83
- })
84
- );
85
-
86
- // TODO: replace with eip6963
87
- const provider = options.provider || window?.ethereum;
88
- if (provider) {
89
- this.trackProvider(provider);
90
- }
91
-
92
- this.trackFirstPageHit();
93
- this.trackPageHits();
94
- }
95
-
96
- static async init(
97
- writeKey: string,
98
- options?: Options
99
- ): Promise<FormoAnalytics> {
100
- initStorageManager(writeKey);
101
- const analytics = new FormoAnalytics(writeKey, options);
102
-
103
- // Auto-detect wallet provider
104
- analytics._providers = await analytics.getProviders();
105
- await analytics.detectWallets(analytics._providers);
106
-
107
- return analytics;
108
- }
109
-
110
- /*
111
- Public SDK functions
112
- */
113
-
114
- /**
115
- * Emits a page visit event with the current URL information, fire on page change.
116
- * @param {string} category - The category of the page
117
- * @param {string} name - The name of the page
118
- * @param {Record<string, any>} properties - Additional properties to include
119
- * @param {Record<string, any>} context - Additional context to include
120
- * @returns {Promise<void>}
121
- */
122
- public async page(
123
- category?: string,
124
- name?: string,
125
- properties?: IFormoEventProperties,
126
- context?: IFormoEventContext
127
- ): Promise<void> {
128
- await this.trackPageHit(category, name, properties, context);
129
- }
130
-
131
- /**
132
- * Reset the current user session.
133
- * @returns {void}
134
- */
135
- public reset(): void {
136
- this.currentUserId = undefined;
137
- cookie().remove(LOCAL_ANONYMOUS_ID_KEY);
138
- cookie().remove(SESSION_USER_ID_KEY);
139
- }
140
-
141
- /**
142
- * Emits a connect wallet event.
143
- * @param {ChainID} params.chainId
144
- * @param {Address} params.address
145
- * @param {IFormoEventProperties} properties
146
- * @param {IFormoEventContext} context
147
- * @param {(...args: unknown[]) => void} callback
148
- * @throws {Error} If chainId or address is empty
149
- * @returns {Promise<void>}
150
- */
151
- async connect(
152
- {
153
- chainId,
154
- address,
155
- }: {
156
- chainId: ChainID;
157
- address: Address;
158
- },
159
- properties?: IFormoEventProperties,
160
- context?: IFormoEventContext,
161
- callback?: (...args: unknown[]) => void
162
- ): Promise<void> {
163
- if (!chainId) {
164
- logger.warn("Connect: Chain ID cannot be empty");
165
- }
166
- if (!address) {
167
- logger.warn("Connect: Address cannot be empty");
168
- }
169
-
170
- this.currentChainId = chainId;
171
- this.currentAddress = address;
172
-
173
- await this.trackEvent(
174
- EventType.CONNECT,
175
- {
176
- chainId,
177
- address,
178
- },
179
- properties,
180
- context,
181
- callback
182
- );
183
- }
184
-
185
- /**
186
- * Emits a wallet disconnect event.
187
- * @param {ChainID} params.chainId
188
- * @param {Address} params.address
189
- * @param {IFormoEventProperties} properties
190
- * @param {IFormoEventContext} context
191
- * @param {(...args: unknown[]) => void} callback
192
- * @returns {Promise<void>}
193
- */
194
- async disconnect(
195
- params?: {
196
- chainId?: ChainID;
197
- address?: Address;
198
- },
199
- properties?: IFormoEventProperties,
200
- context?: IFormoEventContext,
201
- callback?: (...args: unknown[]) => void
202
- ): Promise<void> {
203
- const address = params?.address || this.currentAddress;
204
- const chainId = params?.chainId || this.currentChainId;
205
-
206
- await this.handleDisconnect(
207
- chainId,
208
- address,
209
- properties,
210
- context,
211
- callback
212
- );
213
- }
214
-
215
- /**
216
- * Emits a chain network change event.
217
- * @param {ChainID} params.chainId
218
- * @param {Address} params.address
219
- * @param {IFormoEventProperties} properties
220
- * @param {IFormoEventContext} context
221
- * @param {(...args: unknown[]) => void} callback
222
- * @throws {Error} If chainId is empty, zero, or not a valid number
223
- * @throws {Error} If no address is provided and no previous address is recorded
224
- * @returns {Promise<void>}
225
- */
226
- async chain(
227
- {
228
- chainId,
229
- address,
230
- }: {
231
- chainId: ChainID;
232
- address?: Address;
233
- },
234
- properties?: IFormoEventProperties,
235
- context?: IFormoEventContext,
236
- callback?: (...args: unknown[]) => void
237
- ): Promise<void> {
238
- if (!chainId || Number(chainId) === 0) {
239
- throw new Error("FormoAnalytics::chain: chainId cannot be empty or 0");
240
- }
241
- if (isNaN(Number(chainId))) {
242
- throw new Error(
243
- "FormoAnalytics::chain: chainId must be a valid decimal number"
244
- );
245
- }
246
- if (!address && !this.currentAddress) {
247
- throw new Error(
248
- "FormoAnalytics::chain: address was empty and no previous address has been recorded"
249
- );
250
- }
251
-
252
- this.currentChainId = chainId;
253
-
254
- await this.trackEvent(
255
- EventType.CHAIN,
256
- {
257
- chainId,
258
- address: address || this.currentAddress,
259
- },
260
- properties,
261
- context,
262
- callback
263
- );
264
- }
265
-
266
- /**
267
- * Emits a signature event.
268
- * @param {SignatureStatus} params.status - requested, confirmed, rejected
269
- * @param {ChainID} params.chainId
270
- * @param {Address} params.address
271
- * @param {string} params.message
272
- * @param {string} params.signatureHash - only provided if status is confirmed
273
- * @param {IFormoEventProperties} properties
274
- * @param {IFormoEventContext} context
275
- * @param {(...args: unknown[]) => void} callback
276
- * @returns {Promise<void>}
277
- */
278
- async signature(
279
- {
280
- status,
281
- chainId,
282
- address,
283
- message,
284
- signatureHash,
285
- }: {
286
- status: SignatureStatus;
287
- chainId?: ChainID;
288
- address: Address;
289
- message: string;
290
- signatureHash?: string;
291
- },
292
- properties?: IFormoEventProperties,
293
- context?: IFormoEventContext,
294
- callback?: (...args: unknown[]) => void
295
- ): Promise<void> {
296
- await this.trackEvent(
297
- EventType.SIGNATURE,
298
- {
299
- status,
300
- chainId,
301
- address,
302
- message,
303
- ...(signatureHash && { signatureHash }),
304
- },
305
- properties,
306
- context,
307
- callback
308
- );
309
- }
310
-
311
- /**
312
- * Emits a transaction event.
313
- * @param {TransactionStatus} params.status - started, broadcasted, rejected
314
- * @param {ChainID} params.chainId
315
- * @param {Address} params.address
316
- * @param {string} params.data
317
- * @param {string} params.to
318
- * @param {string} params.value
319
- * @param {string} params.transactionHash - only provided if status is broadcasted
320
- * @param {IFormoEventProperties} properties
321
- * @param {IFormoEventContext} context
322
- * @param {(...args: unknown[]) => void} callback
323
- * @returns {Promise<void>}
324
- */
325
- async transaction(
326
- {
327
- status,
328
- chainId,
329
- address,
330
- data,
331
- to,
332
- value,
333
- transactionHash,
334
- }: {
335
- status: TransactionStatus;
336
- chainId: ChainID;
337
- address: Address;
338
- data?: string;
339
- to?: string;
340
- value?: string;
341
- transactionHash?: string;
342
- },
343
- properties?: IFormoEventProperties,
344
- context?: IFormoEventContext,
345
- callback?: (...args: unknown[]) => void
346
- ): Promise<void> {
347
- await this.trackEvent(
348
- EventType.TRANSACTION,
349
- {
350
- status,
351
- chainId,
352
- address,
353
- data,
354
- to,
355
- value,
356
- ...(transactionHash && { transactionHash }),
357
- },
358
- properties,
359
- context,
360
- callback
361
- );
362
- }
363
-
364
- /**
365
- * Emits an identify event with current wallet address and provider info.
366
- * @param {string} params.address
367
- * @param {string} params.userId
368
- * @param {string} params.rdns
369
- * @param {string} params.providerName
370
- * @param {IFormoEventProperties} properties
371
- * @param {IFormoEventContext} context
372
- * @param {(...args: unknown[]) => void} callback
373
- * @returns {Promise<void>}
374
- */
375
- async identify(
376
- params?: {
377
- address?: Address;
378
- providerName?: string;
379
- userId?: string;
380
- rdns?: string;
381
- },
382
- properties?: IFormoEventProperties,
383
- context?: IFormoEventContext,
384
- callback?: (...args: unknown[]) => void
385
- ): Promise<void> {
386
- try {
387
- if (!params) {
388
- // If no params provided, auto-identify
389
- logger.info(
390
- "Auto-identifying with providers:",
391
- this._providers.map((p) => p.info.name)
392
- );
393
- for (const providerDetail of this._providers) {
394
- const provider = providerDetail.provider;
395
- if (!provider) continue;
396
-
397
- try {
398
- const address = await this.getAddress(provider);
399
- if (address) {
400
- logger.info(
401
- "Auto-identifying",
402
- address,
403
- providerDetail.info.name,
404
- providerDetail.info.rdns
405
- );
406
- // NOTE: do not set this.currentAddress without explicit connect or identify
407
- await this.identify(
408
- {
409
- address,
410
- providerName: providerDetail.info.name,
411
- rdns: providerDetail.info.rdns,
412
- },
413
- properties,
414
- context,
415
- callback
416
- );
417
- }
418
- } catch (err) {
419
- logger.error(
420
- `Failed to identify provider ${providerDetail.info.name}:`,
421
- err
422
- );
423
- }
424
- }
425
- return;
426
- }
427
-
428
- // Explicit identify
429
- const { userId, address, providerName, rdns } = params;
430
- logger.info("Identify", address, userId, providerName, rdns);
431
- if (address) this.currentAddress = address;
432
- if (userId) {
433
- this.currentUserId = userId;
434
- cookie().set(SESSION_USER_ID_KEY, userId);
435
- }
436
-
437
- await this.trackEvent(
438
- EventType.IDENTIFY,
439
- {
440
- address,
441
- providerName,
442
- userId,
443
- rdns,
444
- },
445
- properties,
446
- context,
447
- callback
448
- );
449
- } catch (e) {
450
- logger.log("identify error", e);
451
- }
452
- }
453
-
454
- /**
455
- * Emits a detect wallet event with current wallet provider info.
456
- * @param {string} params.providerName
457
- * @param {string} params.rdns
458
- * @param {IFormoEventProperties} properties
459
- * @param {IFormoEventContext} context
460
- * @param {(...args: unknown[]) => void} callback
461
- * @returns {Promise<void>}
462
- */
463
- async detect(
464
- {
465
- providerName,
466
- rdns,
467
- }: {
468
- providerName: string;
469
- rdns: string;
470
- },
471
- properties?: IFormoEventProperties,
472
- context?: IFormoEventContext,
473
- callback?: (...args: unknown[]) => void
474
- ): Promise<void> {
475
- if (this.session.isWalletDetected(rdns))
476
- return logger.warn(
477
- `Detect: Wallet ${providerName} already detected in this session`
478
- );
479
-
480
- this.session.markWalletDetected(rdns);
481
- await this.trackEvent(
482
- EventType.DETECT,
483
- {
484
- providerName,
485
- rdns,
486
- },
487
- properties,
488
- context,
489
- callback
490
- );
491
- }
492
-
493
- /**
494
- * Emits a custom user event with custom properties.
495
- * @param {string} event The name of the tracked event
496
- * @param {IFormoEventProperties} properties
497
- * @param {IFormoEventContext} context
498
- * @param {(...args: unknown[]) => void} callback
499
- * @returns {Promise<void>}
500
- */
501
- async track(
502
- event: string,
503
- properties?: IFormoEventProperties,
504
- context?: IFormoEventContext,
505
- callback?: (...args: unknown[]) => void
506
- ): Promise<void> {
507
- await this.trackEvent(
508
- EventType.TRACK,
509
- { event },
510
- properties,
511
- context,
512
- callback
513
- );
514
- }
515
-
516
- /*
517
- SDK tracking and event listener functions
518
- */
519
-
520
- private trackProvider(provider: EIP1193Provider): void {
521
- try {
522
- if (provider === this._provider) {
523
- logger.warn("TrackProvider: Provider already tracked.");
524
- return;
525
- }
526
-
527
- this.currentChainId = undefined;
528
- this.currentAddress = undefined;
529
-
530
- if (this._provider) {
531
- const actions = Object.keys(this._providerListeners);
532
- for (const action of actions) {
533
- this._provider.removeListener(
534
- action,
535
- this._providerListeners[action]
536
- );
537
- delete this._providerListeners[action];
538
- }
539
- }
540
-
541
- this._provider = provider;
542
-
543
- // Register listeners for web3 provider events
544
- this.registerAddressChangedListener();
545
- this.registerChainChangedListener();
546
- this.registerSignatureListener();
547
- this.registerTransactionListener();
548
- } catch (error) {
549
- logger.error("Error tracking provider:", error);
550
- }
551
- }
552
-
553
- private registerAddressChangedListener(): void {
554
- const listener = (...args: unknown[]) =>
555
- this.onAddressChanged(args[0] as string[]);
556
-
557
- this._provider?.on("accountsChanged", listener);
558
- this._providerListeners["accountsChanged"] = listener;
559
-
560
- const onAddressDisconnected = this.onAddressDisconnected.bind(this);
561
- this._provider?.on("disconnect", onAddressDisconnected);
562
- this._providerListeners["disconnect"] = onAddressDisconnected;
563
- }
564
-
565
- private registerChainChangedListener(): void {
566
- const listener = (...args: unknown[]) =>
567
- this.onChainChanged(args[0] as string);
568
- this.provider?.on("chainChanged", listener);
569
- this._providerListeners["chainChanged"] = listener;
570
- }
571
-
572
- private registerSignatureListener(): void {
573
- if (!this.provider) {
574
- logger.error("Provider not found for signature tracking");
575
- return;
576
- }
577
- if (
578
- Object.getOwnPropertyDescriptor(this.provider, "request")?.writable ===
579
- false
580
- ) {
581
- logger.warn("Provider.request is not writable");
582
- return;
583
- }
584
-
585
- const request = this.provider.request.bind(this.provider);
586
- this.provider.request = async <T>({
587
- method,
588
- params,
589
- }: RequestArguments): Promise<T | null | undefined> => {
590
- if (
591
- Array.isArray(params) &&
592
- ["eth_signTypedData_v4", "personal_sign"].includes(method)
593
- ) {
594
- // Emit signature request event
595
- this.signature({
596
- status: SignatureStatus.REQUESTED,
597
- ...this.buildSignatureEventPayload(method, params),
598
- });
599
-
600
- try {
601
- const response = (await request({ method, params })) as T;
602
- if (response) {
603
- // Emit signature confirmed event
604
- this.signature({
605
- status: SignatureStatus.CONFIRMED,
606
- ...this.buildSignatureEventPayload(method, params, response),
607
- });
608
- }
609
- return response;
610
- } catch (error) {
611
- const rpcError = error as RPCError;
612
- if (rpcError && rpcError?.code === 4001) {
613
- // Emit signature rejected event
614
- this.signature({
615
- status: SignatureStatus.REJECTED,
616
- ...this.buildSignatureEventPayload(method, params),
617
- });
618
- }
619
- throw error;
620
- }
621
- }
622
- return request({ method, params });
623
- };
624
- return;
625
- }
626
-
627
- private registerTransactionListener(): void {
628
- if (!this.provider) {
629
- logger.error("Provider not found for transaction tracking");
630
- return;
631
- }
632
- if (
633
- Object.getOwnPropertyDescriptor(this.provider, "request")?.writable ===
634
- false
635
- ) {
636
- logger.warn("Provider.request is not writable");
637
- return;
638
- }
639
- const request = this.provider.request.bind(this.provider);
640
- this.provider.request = async <T>({
641
- method,
642
- params,
643
- }: RequestArguments): Promise<T | null | undefined> => {
644
- if (
645
- Array.isArray(params) &&
646
- method === "eth_sendTransaction" &&
647
- params[0]
648
- ) {
649
- // Track transaction start
650
- const payload = await this.buildTransactionEventPayload(params);
651
- this.transaction({ status: TransactionStatus.STARTED, ...payload });
652
-
653
- try {
654
- // Wait for the transaction hash
655
- const transactionHash = (await request({ method, params })) as string;
656
-
657
- // Track transaction broadcast
658
- this.transaction({
659
- status: TransactionStatus.BROADCASTED,
660
- ...payload,
661
- transactionHash,
662
- });
663
-
664
- return;
665
- } catch (error) {
666
- logger.error("Transaction error:", error);
667
- const rpcError = error as RPCError;
668
- if (rpcError && rpcError?.code === 4001) {
669
- // Emit transaction rejected event
670
- this.transaction({
671
- status: TransactionStatus.REJECTED,
672
- ...payload,
673
- });
674
- }
675
- throw error;
676
- }
677
- }
678
-
679
- return request({ method, params });
680
- };
681
-
682
- return;
683
- }
684
-
685
- private async onAddressChanged(addresses: Address[]): Promise<void> {
686
- if (addresses.length > 0) {
687
- this.onAddressConnected(addresses[0]);
688
- } else {
689
- this.onAddressDisconnected();
690
- }
691
- }
692
-
693
- private async onAddressConnected(address: Address): Promise<void> {
694
- if (address === this.currentAddress)
695
- // We have already reported this address
696
- return;
697
-
698
- this.currentAddress = address;
699
-
700
- this.currentChainId = await this.getCurrentChainId();
701
- this.connect({ chainId: this.currentChainId, address });
702
- }
703
-
704
- private async handleDisconnect(
705
- chainId?: ChainID,
706
- address?: Address,
707
- properties?: IFormoEventProperties,
708
- context?: IFormoEventContext,
709
- callback?: (...args: unknown[]) => void
710
- ): Promise<void> {
711
- const payload = {
712
- chainId: chainId || this.currentChainId,
713
- address: address || this.currentAddress,
714
- };
715
- this.currentChainId = undefined;
716
- this.currentAddress = undefined;
717
- cookie().remove(SESSION_USER_ID_KEY);
718
-
719
- await this.trackEvent(
720
- EventType.DISCONNECT,
721
- payload,
722
- properties,
723
- context,
724
- callback
725
- );
726
- }
727
-
728
- private async onAddressDisconnected(): Promise<void> {
729
- await this.handleDisconnect(this.currentChainId, this.currentAddress);
730
- }
731
-
732
- private async onChainChanged(chainIdHex: string): Promise<void> {
733
- this.currentChainId = parseInt(chainIdHex);
734
- if (!this.currentAddress) {
735
- if (!this.provider) {
736
- logger.info(
737
- "OnChainChanged: Provider not found. CHAIN_CHANGED not reported"
738
- );
739
- return Promise.resolve();
740
- }
741
-
742
- const address = await this.getAddress();
743
- if (!address) {
744
- logger.info(
745
- "OnChainChanged: Unable to fetch or store connected address"
746
- );
747
- return Promise.resolve();
748
- }
749
- this.currentAddress = address;
750
- }
751
-
752
- // Proceed only if the address exists
753
- if (this.currentAddress) {
754
- return this.chain({
755
- chainId: this.currentChainId,
756
- address: this.currentAddress,
757
- });
758
- } else {
759
- logger.info(
760
- "OnChainChanged: Current connected address is null despite fetch attempt"
761
- );
762
- }
763
- }
764
-
765
- private async trackFirstPageHit(): Promise<void> {
766
- if (cookie().get(SESSION_CURRENT_URL_KEY) === null) {
767
- cookie().set(SESSION_CURRENT_URL_KEY, window.location.href);
768
- }
769
-
770
- return this.trackPageHit();
771
- }
772
-
773
- private async trackPageHits(): Promise<void> {
774
- const oldPushState = history.pushState;
775
- history.pushState = function pushState(...args) {
776
- const ret = oldPushState.apply(this, args);
777
- window.dispatchEvent(new window.Event("locationchange"));
778
- return ret;
779
- };
780
-
781
- const oldReplaceState = history.replaceState;
782
- history.replaceState = function replaceState(...args) {
783
- const ret = oldReplaceState.apply(this, args);
784
- window.dispatchEvent(new window.Event("locationchange"));
785
- return ret;
786
- };
787
-
788
- window.addEventListener("popstate", () => this.onLocationChange());
789
- window.addEventListener("locationchange", () => this.onLocationChange());
790
- }
791
-
792
- private async onLocationChange(): Promise<void> {
793
- const currentUrl = cookie().get(SESSION_CURRENT_URL_KEY);
794
-
795
- if (currentUrl !== window.location.href) {
796
- cookie().set(SESSION_CURRENT_URL_KEY, window.location.href);
797
- this.trackPageHit();
798
- }
799
- }
800
-
801
- private async trackPageHit(
802
- category?: string,
803
- name?: string,
804
- properties?: IFormoEventProperties,
805
- context?: IFormoEventContext,
806
- callback?: (...args: unknown[]) => void
807
- ): Promise<void> {
808
- if (!this.config.trackLocalhost && isLocalhost()) {
809
- return logger.warn(
810
- "Track page hit: Ignoring event because website is running locally"
811
- );
812
- }
813
-
814
- setTimeout(async () => {
815
- this.trackEvent(
816
- EventType.PAGE,
817
- {
818
- category,
819
- name,
820
- },
821
- properties,
822
- context,
823
- callback
824
- );
825
- }, 300);
826
- }
827
-
828
- private async trackEvent(
829
- type: TEventType,
830
- payload?: any,
831
- properties?: IFormoEventProperties,
832
- context?: IFormoEventContext,
833
- callback?: (...args: unknown[]) => void
834
- ): Promise<void> {
835
- try {
836
- this.eventManager.addEvent(
837
- {
838
- type,
839
- ...payload,
840
- properties,
841
- context,
842
- callback,
843
- },
844
- this.currentAddress,
845
- this.currentUserId
846
- );
847
- } catch (error) {
848
- logger.error("Error tracking event:", error);
849
- }
850
- }
851
-
852
- /*
853
- Utility functions
854
- */
855
-
856
- private async getProviders(): Promise<readonly EIP6963ProviderDetail[]> {
857
- const store = createStore();
858
- let providers = store.getProviders();
859
- store.subscribe((providerDetails) => {
860
- providers = providerDetails;
861
- this._providers = providers;
862
- });
863
-
864
- // Fallback to injected provider if no providers are found
865
- if (providers.length === 0) {
866
- this._providers = window?.ethereum ? [window.ethereum] : [];
867
- return this._providers;
868
- }
869
- this._providers = providers;
870
- return providers;
871
- }
872
-
873
- get providers(): readonly EIP6963ProviderDetail[] {
874
- return this._providers;
875
- }
876
-
877
- private async detectWallets(
878
- providers: readonly EIP6963ProviderDetail[]
879
- ): Promise<void> {
880
- try {
881
- for (const eip6963ProviderDetail of providers) {
882
- await this.detect({
883
- providerName: eip6963ProviderDetail?.info.name,
884
- rdns: eip6963ProviderDetail?.info.rdns,
885
- });
886
- }
887
- } catch (err) {
888
- logger.error("Error detect all wallets:", err);
889
- }
890
- }
891
-
892
- get provider(): EIP1193Provider | undefined {
893
- return this._provider;
894
- }
895
-
896
- private async getAddress(
897
- provider?: EIP1193Provider
898
- ): Promise<Address | null> {
899
- if (this.currentAddress) return this.currentAddress;
900
- const p = provider || this.provider;
901
- if (!p) {
902
- logger.info("The provider is not set");
903
- return null;
904
- }
905
-
906
- try {
907
- const accounts = await this.getAccounts(p);
908
- if (accounts && accounts.length > 0) {
909
- if (isAddress(accounts[0])) {
910
- return accounts[0];
911
- }
912
- }
913
- } catch (err) {
914
- logger.error("Failed to fetch accounts from provider:", err);
915
- return null;
916
- }
917
- return null;
918
- }
919
-
920
- private async getAccounts(
921
- provider?: EIP1193Provider
922
- ): Promise<Address[] | null> {
923
- const p = provider || this.provider;
924
- try {
925
- const res: string[] | null | undefined = await p?.request({
926
- method: "eth_accounts",
927
- });
928
- if (!res || res.length === 0) return null;
929
- return res.filter((e) => isAddress(e));
930
- } catch (err) {
931
- if ((err as any).code !== 4001) {
932
- logger.error(
933
- "FormoAnalytics::getAccounts: eth_accounts threw an error",
934
- err
935
- );
936
- }
937
- return null;
938
- }
939
- }
940
-
941
- private async getCurrentChainId(): Promise<number> {
942
- if (!this.provider) {
943
- logger.error("Provider not set for chain ID");
944
- }
945
-
946
- let chainIdHex;
947
- try {
948
- chainIdHex = await this.provider?.request<string>({
949
- method: "eth_chainId",
950
- });
951
- if (!chainIdHex) {
952
- logger.info("Chain id not found");
953
- return 0;
954
- }
955
- return parseInt(chainIdHex as string, 16);
956
- } catch (err) {
957
- logger.error("eth_chainId threw an error:", err);
958
- return 0;
959
- }
960
- }
961
-
962
- private buildSignatureEventPayload(
963
- method: string,
964
- params: unknown[],
965
- response?: unknown
966
- ) {
967
- const basePayload = {
968
- chainId: this.currentChainId,
969
- address:
970
- method === "personal_sign"
971
- ? (params[1] as Address)
972
- : (params[0] as Address),
973
- };
974
-
975
- if (method === "personal_sign") {
976
- const message = Buffer.from(
977
- (params[0] as string).slice(2),
978
- "hex"
979
- ).toString("utf8");
980
- return {
981
- ...basePayload,
982
- message,
983
- ...(response ? { signatureHash: response as string } : {}),
984
- };
985
- }
986
-
987
- return {
988
- ...basePayload,
989
- message: params[1] as string,
990
- ...(response ? { signatureHash: response as string } : {}),
991
- };
992
- }
993
-
994
- private async buildTransactionEventPayload(params: unknown[]) {
995
- const { data, from, to, value } = params[0] as {
996
- data: string;
997
- from: string;
998
- to: string;
999
- value: string;
1000
- };
1001
- return {
1002
- chainId: this.currentChainId || (await this.getCurrentChainId()),
1003
- data,
1004
- address: from,
1005
- to,
1006
- value,
1007
- };
1008
- }
1009
- }
1010
-
1011
- interface IFormoAnalyticsSession {
1012
- isWalletDetected(rdns: string): boolean;
1013
- markWalletDetected(rdns: string): void;
1014
- }
1015
-
1016
- class FormoAnalyticsSession implements IFormoAnalyticsSession {
1017
- public isWalletDetected(rdns: string): boolean {
1018
- const rdnses = cookie().get(SESSION_WALLET_DETECTED_KEY)?.split(",") || [];
1019
- return rdnses.includes(rdns);
1020
- }
1021
-
1022
- public markWalletDetected(rdns: string): void {
1023
- const rdnses = cookie().get(SESSION_WALLET_DETECTED_KEY)?.split(",") || [];
1024
- rdnses.push(rdns);
1025
- cookie().set(SESSION_WALLET_DETECTED_KEY, rdnses.join(","), {
1026
- // by the end of the day
1027
- expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
1028
- path: "/",
1029
- });
1030
- }
1031
- }