@formo/analytics-react-native 1.1.0 → 1.2.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.
- package/lib/commonjs/FormoAnalytics.js +75 -19
- package/lib/commonjs/FormoAnalytics.js.map +1 -1
- package/lib/commonjs/lib/event/EventFactory.js +9 -4
- package/lib/commonjs/lib/event/EventFactory.js.map +1 -1
- package/lib/commonjs/lib/event/EventManager.js +12 -4
- package/lib/commonjs/lib/event/EventManager.js.map +1 -1
- package/lib/commonjs/lib/event/EventQueue.js +5 -2
- package/lib/commonjs/lib/event/EventQueue.js.map +1 -1
- package/lib/commonjs/lib/installReferrer/index.js +16 -9
- package/lib/commonjs/lib/installReferrer/index.js.map +1 -1
- package/lib/commonjs/lib/wagmi/WagmiEventHandler.js +20 -0
- package/lib/commonjs/lib/wagmi/WagmiEventHandler.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/FormoAnalytics.js +76 -20
- package/lib/module/FormoAnalytics.js.map +1 -1
- package/lib/module/lib/event/EventFactory.js +8 -3
- package/lib/module/lib/event/EventFactory.js.map +1 -1
- package/lib/module/lib/event/EventManager.js +13 -5
- package/lib/module/lib/event/EventManager.js.map +1 -1
- package/lib/module/lib/event/EventQueue.js +5 -2
- package/lib/module/lib/event/EventQueue.js.map +1 -1
- package/lib/module/lib/installReferrer/index.js +17 -9
- package/lib/module/lib/installReferrer/index.js.map +1 -1
- package/lib/module/lib/wagmi/WagmiEventHandler.js +20 -0
- package/lib/module/lib/wagmi/WagmiEventHandler.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/FormoAnalytics.d.ts +4 -3
- package/lib/typescript/commonjs/FormoAnalytics.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/event/EventFactory.d.ts +3 -1
- package/lib/typescript/commonjs/lib/event/EventFactory.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/event/EventManager.d.ts +1 -1
- package/lib/typescript/commonjs/lib/event/EventManager.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/event/EventQueue.d.ts +2 -1
- package/lib/typescript/commonjs/lib/event/EventQueue.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/event/types.d.ts +2 -1
- package/lib/typescript/commonjs/lib/event/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/installReferrer/index.d.ts +1 -0
- package/lib/typescript/commonjs/lib/installReferrer/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/lib/wagmi/WagmiEventHandler.d.ts +2 -0
- package/lib/typescript/commonjs/lib/wagmi/WagmiEventHandler.d.ts.map +1 -1
- package/lib/typescript/commonjs/version.d.ts +1 -1
- package/lib/typescript/module/FormoAnalytics.d.ts +4 -3
- package/lib/typescript/module/FormoAnalytics.d.ts.map +1 -1
- package/lib/typescript/module/lib/event/EventFactory.d.ts +3 -1
- package/lib/typescript/module/lib/event/EventFactory.d.ts.map +1 -1
- package/lib/typescript/module/lib/event/EventManager.d.ts +1 -1
- package/lib/typescript/module/lib/event/EventManager.d.ts.map +1 -1
- package/lib/typescript/module/lib/event/EventQueue.d.ts +2 -1
- package/lib/typescript/module/lib/event/EventQueue.d.ts.map +1 -1
- package/lib/typescript/module/lib/event/types.d.ts +2 -1
- package/lib/typescript/module/lib/event/types.d.ts.map +1 -1
- package/lib/typescript/module/lib/installReferrer/index.d.ts +1 -0
- package/lib/typescript/module/lib/installReferrer/index.d.ts.map +1 -1
- package/lib/typescript/module/lib/wagmi/WagmiEventHandler.d.ts +2 -0
- package/lib/typescript/module/lib/wagmi/WagmiEventHandler.d.ts.map +1 -1
- package/lib/typescript/module/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/FormoAnalytics.ts +99 -20
- package/src/lib/event/EventFactory.ts +10 -3
- package/src/lib/event/EventManager.ts +18 -5
- package/src/lib/event/EventQueue.ts +8 -3
- package/src/lib/event/types.ts +3 -1
- package/src/lib/installReferrer/index.ts +20 -10
- package/src/lib/wagmi/WagmiEventHandler.ts +24 -0
- package/src/version.ts +1 -1
package/src/FormoAnalytics.ts
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
TransactionStatus,
|
|
41
41
|
} from "./types";
|
|
42
42
|
import { validateAddress } from "./utils";
|
|
43
|
-
import { parseTrafficSource, updateStoredTrafficSource } from "./utils/trafficSource";
|
|
43
|
+
import { clearTrafficSource, parseTrafficSource, updateStoredTrafficSource } from "./utils/trafficSource";
|
|
44
44
|
import { captureInstallReferrer } from "./lib/installReferrer";
|
|
45
45
|
import { Linking, EmitterSubscription } from "react-native";
|
|
46
46
|
|
|
@@ -60,6 +60,9 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
60
60
|
private crashReporter?: CrashReporter;
|
|
61
61
|
private initialDeepLinkUrl?: string;
|
|
62
62
|
private linkingSubscription?: EmitterSubscription;
|
|
63
|
+
private walletGeneration = 0;
|
|
64
|
+
private chainGeneration = 0;
|
|
65
|
+
private consentGeneration = 0;
|
|
63
66
|
|
|
64
67
|
config: Config;
|
|
65
68
|
currentChainId?: ChainID;
|
|
@@ -86,7 +89,18 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
86
89
|
this.detect = this.detect.bind(this);
|
|
87
90
|
this.track = this.track.bind(this);
|
|
88
91
|
this.screen = this.screen.bind(this);
|
|
92
|
+
this.reset = this.reset.bind(this);
|
|
93
|
+
this.cleanup = this.cleanup.bind(this);
|
|
94
|
+
this.flush = this.flush.bind(this);
|
|
95
|
+
this.setTrafficSourceFromUrl = this.setTrafficSourceFromUrl.bind(this);
|
|
96
|
+
this.optOutTracking = this.optOutTracking.bind(this);
|
|
97
|
+
this.optInTracking = this.optInTracking.bind(this);
|
|
98
|
+
this.hasOptedOutTracking = this.hasOptedOutTracking.bind(this);
|
|
99
|
+
this.pushNotificationReceived = this.pushNotificationReceived.bind(this);
|
|
100
|
+
this.pushNotificationTapped = this.pushNotificationTapped.bind(this);
|
|
101
|
+
this.pushNotificationBounced = this.pushNotificationBounced.bind(this);
|
|
89
102
|
this.isAutocaptureEnabled = this.isAutocaptureEnabled.bind(this);
|
|
103
|
+
this.isAttributionEnabled = this.isAttributionEnabled.bind(this);
|
|
90
104
|
|
|
91
105
|
// Initialize logger
|
|
92
106
|
Logger.init({
|
|
@@ -104,10 +118,15 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
104
118
|
});
|
|
105
119
|
|
|
106
120
|
// Initialize event manager
|
|
107
|
-
this.eventManager = new EventManager(
|
|
121
|
+
this.eventManager = new EventManager(
|
|
122
|
+
this.eventQueue,
|
|
123
|
+
options,
|
|
124
|
+
() => !this.hasOptedOutTracking()
|
|
125
|
+
);
|
|
108
126
|
|
|
109
127
|
// Check consent status
|
|
110
128
|
if (this.hasOptedOutTracking()) {
|
|
129
|
+
clearTrafficSource();
|
|
111
130
|
logger.info("User has previously opted out of tracking");
|
|
112
131
|
}
|
|
113
132
|
|
|
@@ -172,9 +191,13 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
172
191
|
|
|
173
192
|
if (analytics.isAttributionEnabled("installReferrer")) {
|
|
174
193
|
try {
|
|
194
|
+
const consentGeneration = analytics.consentGeneration;
|
|
175
195
|
await captureInstallReferrer({
|
|
176
196
|
customRefParams: analytics.options.referral?.queryParams,
|
|
177
197
|
pathPattern: analytics.options.referral?.pathPattern,
|
|
198
|
+
canCapture: () =>
|
|
199
|
+
analytics.consentGeneration === consentGeneration &&
|
|
200
|
+
!analytics.hasOptedOutTracking(),
|
|
178
201
|
});
|
|
179
202
|
} catch (error) {
|
|
180
203
|
logger.debug("FormoAnalytics: install referrer capture failed", error);
|
|
@@ -226,9 +249,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
226
249
|
* so attribution is in storage before the first lifecycle event fires.
|
|
227
250
|
*/
|
|
228
251
|
private async startDeepLinkCapture(): Promise<void> {
|
|
252
|
+
const consentGeneration = this.consentGeneration;
|
|
229
253
|
try {
|
|
230
254
|
const url = await Linking.getInitialURL();
|
|
231
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
url &&
|
|
257
|
+
this.consentGeneration === consentGeneration &&
|
|
258
|
+
!this.hasOptedOutTracking()
|
|
259
|
+
) {
|
|
232
260
|
if (this.isAttributionEnabled("deeplinks")) {
|
|
233
261
|
this.setTrafficSourceFromUrl(url);
|
|
234
262
|
}
|
|
@@ -243,7 +271,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
243
271
|
|
|
244
272
|
// Runtime deep links (foreground opens, universal links).
|
|
245
273
|
this.linkingSubscription = Linking.addEventListener("url", (event) => {
|
|
246
|
-
if (!event?.url) return;
|
|
274
|
+
if (!event?.url || this.hasOptedOutTracking()) return;
|
|
247
275
|
// Each behaviour checks its own flag: the hook may exist because only one
|
|
248
276
|
// of them is enabled.
|
|
249
277
|
if (this.isAttributionEnabled("deeplinks")) {
|
|
@@ -367,6 +395,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
367
395
|
* ```
|
|
368
396
|
*/
|
|
369
397
|
public setTrafficSourceFromUrl(url: string): void {
|
|
398
|
+
if (this.hasOptedOutTracking()) return;
|
|
370
399
|
const trafficSource = parseTrafficSource(
|
|
371
400
|
url,
|
|
372
401
|
this.options.referral?.queryParams,
|
|
@@ -379,12 +408,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
379
408
|
logger.debug("Traffic source set from URL:", trafficSource);
|
|
380
409
|
}
|
|
381
410
|
|
|
382
|
-
/**
|
|
383
|
-
* Reset the current user session
|
|
384
|
-
*/
|
|
411
|
+
/** Reset user and wallet state, preserving device identity and attribution. */
|
|
385
412
|
public reset(): void {
|
|
413
|
+
this.walletGeneration++;
|
|
414
|
+
this.chainGeneration++;
|
|
386
415
|
this.currentUserId = undefined;
|
|
387
|
-
|
|
416
|
+
this.currentAddress = undefined;
|
|
417
|
+
this.currentChainId = undefined;
|
|
418
|
+
this.wagmiHandler?.clearIdentity();
|
|
388
419
|
storage().remove(LOCAL_SESSION_ID_KEY);
|
|
389
420
|
storage().remove(LOCAL_SESSION_LAST_ACTIVITY_KEY);
|
|
390
421
|
storage().remove(SESSION_USER_ID_KEY);
|
|
@@ -447,6 +478,9 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
447
478
|
logger.warn(`Connect: Invalid address provided ("${address}")`);
|
|
448
479
|
return;
|
|
449
480
|
}
|
|
481
|
+
if (this.hasOptedOutTracking()) return;
|
|
482
|
+
const walletGeneration = ++this.walletGeneration;
|
|
483
|
+
const chainGeneration = ++this.chainGeneration;
|
|
450
484
|
|
|
451
485
|
// Track event before updating state so connect events TO excluded chains are tracked
|
|
452
486
|
await this.trackEvent(
|
|
@@ -457,8 +491,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
457
491
|
callback
|
|
458
492
|
);
|
|
459
493
|
|
|
460
|
-
this.
|
|
461
|
-
|
|
494
|
+
if (!this.hasOptedOutTracking()) {
|
|
495
|
+
if (walletGeneration === this.walletGeneration) {
|
|
496
|
+
this.currentAddress = validatedAddress;
|
|
497
|
+
}
|
|
498
|
+
if (chainGeneration === this.chainGeneration) {
|
|
499
|
+
this.currentChainId = chainId;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
462
502
|
}
|
|
463
503
|
|
|
464
504
|
/**
|
|
@@ -470,6 +510,8 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
470
510
|
context?: IFormoEventContext,
|
|
471
511
|
callback?: (...args: unknown[]) => void
|
|
472
512
|
): Promise<void> {
|
|
513
|
+
const walletGeneration = ++this.walletGeneration;
|
|
514
|
+
const chainGeneration = ++this.chainGeneration;
|
|
473
515
|
const chainId = params?.chainId || this.currentChainId;
|
|
474
516
|
const address = params?.address || this.currentAddress;
|
|
475
517
|
|
|
@@ -489,8 +531,8 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
489
531
|
callback
|
|
490
532
|
);
|
|
491
533
|
|
|
492
|
-
this.currentAddress = undefined;
|
|
493
|
-
this.currentChainId = undefined;
|
|
534
|
+
if (walletGeneration === this.walletGeneration) this.currentAddress = undefined;
|
|
535
|
+
if (chainGeneration === this.chainGeneration) this.currentChainId = undefined;
|
|
494
536
|
}
|
|
495
537
|
|
|
496
538
|
/**
|
|
@@ -510,22 +552,37 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
510
552
|
logger.warn("FormoAnalytics::chain: chainId must be a valid number");
|
|
511
553
|
return;
|
|
512
554
|
}
|
|
513
|
-
|
|
514
|
-
|
|
555
|
+
const validAddress = address
|
|
556
|
+
? this.validateAndChecksumAddress(address, chainId)
|
|
557
|
+
: this.currentAddress;
|
|
558
|
+
if (!validAddress) {
|
|
559
|
+
logger.warn("FormoAnalytics::chain: address is invalid or unavailable");
|
|
515
560
|
return;
|
|
516
561
|
}
|
|
562
|
+
if (this.hasOptedOutTracking()) return;
|
|
563
|
+
const walletGeneration = address
|
|
564
|
+
? ++this.walletGeneration
|
|
565
|
+
: this.walletGeneration;
|
|
566
|
+
const chainGeneration = ++this.chainGeneration;
|
|
517
567
|
|
|
518
568
|
// Track event before updating currentChainId so shouldTrack uses the previous chain
|
|
519
569
|
// This ensures chain change events TO excluded chains are still tracked
|
|
520
570
|
await this.trackEvent(
|
|
521
571
|
EventType.CHAIN,
|
|
522
|
-
{ chainId, address:
|
|
572
|
+
{ chainId, address: validAddress },
|
|
523
573
|
properties,
|
|
524
574
|
context,
|
|
525
575
|
callback
|
|
526
576
|
);
|
|
527
577
|
|
|
528
|
-
this.
|
|
578
|
+
if (!this.hasOptedOutTracking()) {
|
|
579
|
+
if (chainGeneration === this.chainGeneration) {
|
|
580
|
+
this.currentChainId = chainId;
|
|
581
|
+
}
|
|
582
|
+
if (address && walletGeneration === this.walletGeneration) {
|
|
583
|
+
this.currentAddress = validAddress;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
529
586
|
}
|
|
530
587
|
|
|
531
588
|
/**
|
|
@@ -638,6 +695,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
638
695
|
try {
|
|
639
696
|
const { userId, address, providerName, rdns } = params;
|
|
640
697
|
logger.info("Identify", address, userId, providerName, rdns);
|
|
698
|
+
if (this.hasOptedOutTracking()) return;
|
|
641
699
|
|
|
642
700
|
let validAddress: Address | undefined = undefined;
|
|
643
701
|
if (address) {
|
|
@@ -646,10 +704,12 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
646
704
|
logger.warn(`Identify: Invalid address provided ("${address}")`);
|
|
647
705
|
return;
|
|
648
706
|
}
|
|
707
|
+
this.walletGeneration++;
|
|
649
708
|
this.currentAddress = validAddress;
|
|
650
709
|
// Note: validateAddress returns Solana addresses unchanged (Base58, case-sensitive)
|
|
651
710
|
// and EVM addresses checksummed.
|
|
652
711
|
} else {
|
|
712
|
+
this.walletGeneration++;
|
|
653
713
|
this.currentAddress = undefined;
|
|
654
714
|
}
|
|
655
715
|
|
|
@@ -696,6 +756,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
696
756
|
context?: IFormoEventContext,
|
|
697
757
|
callback?: (...args: unknown[]) => void
|
|
698
758
|
): Promise<void> {
|
|
759
|
+
if (this.hasOptedOutTracking()) return;
|
|
699
760
|
if (this.session.isWalletDetected(rdns)) {
|
|
700
761
|
logger.warn(`Detect: Wallet ${providerName} already detected in this session`);
|
|
701
762
|
return;
|
|
@@ -734,9 +795,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
734
795
|
*/
|
|
735
796
|
public optOutTracking(): void {
|
|
736
797
|
logger.info("Opting out of tracking");
|
|
798
|
+
this.consentGeneration++;
|
|
799
|
+
this.initialDeepLinkUrl = undefined;
|
|
737
800
|
setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
|
|
738
801
|
this.eventQueue.clear();
|
|
739
802
|
this.reset();
|
|
803
|
+
// Consent withdrawal clears device identity and attribution too.
|
|
804
|
+
storage().remove(LOCAL_ANONYMOUS_ID_KEY);
|
|
805
|
+
clearTrafficSource();
|
|
740
806
|
logger.info("Successfully opted out of tracking");
|
|
741
807
|
}
|
|
742
808
|
|
|
@@ -842,7 +908,18 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
842
908
|
callback?: (...args: unknown[]) => void
|
|
843
909
|
): Promise<void> {
|
|
844
910
|
try {
|
|
845
|
-
|
|
911
|
+
const payloadChainId = payload?.chainId as ChainID | undefined;
|
|
912
|
+
const validPayloadChainId =
|
|
913
|
+
typeof payloadChainId === "number" &&
|
|
914
|
+
Number.isFinite(payloadChainId) &&
|
|
915
|
+
payloadChainId > 0
|
|
916
|
+
? payloadChainId
|
|
917
|
+
: undefined;
|
|
918
|
+
const eventChainId =
|
|
919
|
+
type === EventType.CONNECT || type === EventType.CHAIN
|
|
920
|
+
? this.currentChainId
|
|
921
|
+
: validPayloadChainId;
|
|
922
|
+
if (!this.shouldTrack(eventChainId)) {
|
|
846
923
|
logger.info(`Skipping ${type} event due to tracking configuration`);
|
|
847
924
|
return;
|
|
848
925
|
}
|
|
@@ -873,7 +950,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
873
950
|
/**
|
|
874
951
|
* Check if tracking should be enabled
|
|
875
952
|
*/
|
|
876
|
-
private shouldTrack(): boolean {
|
|
953
|
+
private shouldTrack(eventChainId?: ChainID): boolean {
|
|
877
954
|
// Check consent
|
|
878
955
|
if (this.hasOptedOutTracking()) {
|
|
879
956
|
return false;
|
|
@@ -891,11 +968,13 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
891
968
|
!Array.isArray(this.options.tracking)
|
|
892
969
|
) {
|
|
893
970
|
const { excludeChains = [] } = this.options.tracking as TrackingOptions;
|
|
971
|
+
const chainId = eventChainId ?? this.currentChainId;
|
|
894
972
|
|
|
895
973
|
if (
|
|
896
974
|
excludeChains.length > 0 &&
|
|
897
|
-
|
|
898
|
-
|
|
975
|
+
chainId !== undefined &&
|
|
976
|
+
chainId !== null &&
|
|
977
|
+
excludeChains.includes(chainId)
|
|
899
978
|
) {
|
|
900
979
|
return false;
|
|
901
980
|
}
|
|
@@ -210,11 +210,13 @@ export function synthesizeUserAgent(info: {
|
|
|
210
210
|
*/
|
|
211
211
|
class EventFactory implements IEventFactory {
|
|
212
212
|
private options?: Options;
|
|
213
|
+
private canCreate: () => boolean;
|
|
213
214
|
/** Memoised device/app identity — see getDeviceInfo(). */
|
|
214
215
|
private deviceInfoPromise?: Promise<DeviceInfoResult>;
|
|
215
216
|
|
|
216
|
-
constructor(options?: Options) {
|
|
217
|
+
constructor(options?: Options, canCreate: () => boolean = () => true) {
|
|
217
218
|
this.options = options;
|
|
219
|
+
this.canCreate = canCreate;
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
/**
|
|
@@ -508,6 +510,10 @@ class EventFactory implements IEventFactory {
|
|
|
508
510
|
formoEvent: Partial<IFormoEvent>,
|
|
509
511
|
context?: IFormoEventContext
|
|
510
512
|
): Promise<IFormoEvent> {
|
|
513
|
+
if (!this.canCreate()) throw EVENT_CREATION_CANCELLED;
|
|
514
|
+
|
|
515
|
+
const anonymousId = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY);
|
|
516
|
+
const sessionId = getSessionId();
|
|
511
517
|
const commonEventData: Partial<IFormoEvent> = {
|
|
512
518
|
context: await this.generateContext(context),
|
|
513
519
|
original_timestamp: getCurrentTimeFormatted(),
|
|
@@ -517,8 +523,8 @@ class EventFactory implements IEventFactory {
|
|
|
517
523
|
version: VERSION,
|
|
518
524
|
};
|
|
519
525
|
|
|
520
|
-
commonEventData.anonymous_id =
|
|
521
|
-
commonEventData.session_id =
|
|
526
|
+
commonEventData.anonymous_id = anonymousId;
|
|
527
|
+
commonEventData.session_id = sessionId;
|
|
522
528
|
|
|
523
529
|
// Handle address - convert undefined to null for consistency
|
|
524
530
|
// Try EVM first, then Solana fallback (chainId is not always present here).
|
|
@@ -886,3 +892,4 @@ class EventFactory implements IEventFactory {
|
|
|
886
892
|
}
|
|
887
893
|
|
|
888
894
|
export { EventFactory };
|
|
895
|
+
export const EVENT_CREATION_CANCELLED = Symbol("event creation cancelled");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Address, APIEvent, Options } from "../../types";
|
|
2
2
|
import { logger } from "../logger";
|
|
3
|
-
import { EventFactory } from "./EventFactory";
|
|
3
|
+
import { EVENT_CREATION_CANCELLED, EventFactory } from "./EventFactory";
|
|
4
4
|
import { IEventFactory, IEventManager, IEventQueue } from "./types";
|
|
5
5
|
import { isBlockedAddress } from "../../utils/address";
|
|
6
6
|
|
|
@@ -12,9 +12,13 @@ class EventManager implements IEventManager {
|
|
|
12
12
|
eventQueue: IEventQueue;
|
|
13
13
|
eventFactory: IEventFactory;
|
|
14
14
|
|
|
15
|
-
constructor(
|
|
15
|
+
constructor(
|
|
16
|
+
eventQueue: IEventQueue,
|
|
17
|
+
options?: Options,
|
|
18
|
+
canCreate: () => boolean = () => true
|
|
19
|
+
) {
|
|
16
20
|
this.eventQueue = eventQueue;
|
|
17
|
-
this.eventFactory = new EventFactory(options);
|
|
21
|
+
this.eventFactory = new EventFactory(options, canCreate);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -26,7 +30,16 @@ class EventManager implements IEventManager {
|
|
|
26
30
|
userId?: string
|
|
27
31
|
): Promise<void> {
|
|
28
32
|
const { callback, ..._event } = event;
|
|
29
|
-
const
|
|
33
|
+
const generation = this.eventQueue.getGeneration();
|
|
34
|
+
let formoEvent;
|
|
35
|
+
try {
|
|
36
|
+
formoEvent = await this.eventFactory.create(_event, address, userId);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error === EVENT_CREATION_CANCELLED) return;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.eventQueue.getGeneration() !== generation) return;
|
|
30
43
|
|
|
31
44
|
// Check if the final event has a blocked address
|
|
32
45
|
if (formoEvent.address && isBlockedAddress(formoEvent.address)) {
|
|
@@ -43,7 +56,7 @@ class EventManager implements IEventManager {
|
|
|
43
56
|
logger.info(`Events sent successfully: ${(data as unknown[])?.length ?? 0} events`);
|
|
44
57
|
}
|
|
45
58
|
callback?.(err, _, data);
|
|
46
|
-
});
|
|
59
|
+
}, generation);
|
|
47
60
|
}
|
|
48
61
|
}
|
|
49
62
|
|
|
@@ -210,16 +210,17 @@ export class EventQueue implements IEventQueue {
|
|
|
210
210
|
*/
|
|
211
211
|
async enqueue(
|
|
212
212
|
event: IFormoEvent,
|
|
213
|
-
callback?: (...args: unknown[]) => void
|
|
213
|
+
callback?: (...args: unknown[]) => void,
|
|
214
|
+
generation = this.generation
|
|
214
215
|
): Promise<void> {
|
|
215
216
|
if (this.closed) {
|
|
216
217
|
logger.debug("EventQueue: Ignoring event enqueued after cleanup");
|
|
217
218
|
return;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
if (this.generation !== generation) return;
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
callback = callback || noop;
|
|
223
224
|
const message_id = await this.generateMessageId(event);
|
|
224
225
|
|
|
225
226
|
// Hashing is async, so cleanup() can complete while this call is suspended
|
|
@@ -558,6 +559,10 @@ export class EventQueue implements IEventQueue {
|
|
|
558
559
|
logger.debug("EventQueue: Cleared all pending events");
|
|
559
560
|
}
|
|
560
561
|
|
|
562
|
+
public getGeneration(): number {
|
|
563
|
+
return this.generation;
|
|
564
|
+
}
|
|
565
|
+
|
|
561
566
|
/**
|
|
562
567
|
* Give up on the events still queued at teardown, and make sure a send that
|
|
563
568
|
* is still holding a batch cannot put them back.
|
package/src/lib/event/types.ts
CHANGED
|
@@ -96,9 +96,11 @@ export interface IEventManager {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export interface IEventQueue {
|
|
99
|
+
getGeneration(): number;
|
|
99
100
|
enqueue(
|
|
100
101
|
event: IFormoEvent,
|
|
101
|
-
callback?: (...args: unknown[]) => void
|
|
102
|
+
callback?: (...args: unknown[]) => void,
|
|
103
|
+
generation?: number
|
|
102
104
|
): Promise<void>;
|
|
103
105
|
flush(callback?: (...args: unknown[]) => void): Promise<void>;
|
|
104
106
|
clear(): void;
|
|
@@ -39,17 +39,23 @@ import {
|
|
|
39
39
|
import type { ITrafficSource } from "../../types";
|
|
40
40
|
|
|
41
41
|
// Lazy-load the optional native module. Absence is fine — attribution is best-effort.
|
|
42
|
-
|
|
42
|
+
type PlayInstallReferrerModule = {
|
|
43
43
|
getInstallReferrerInfo: (
|
|
44
44
|
cb: (info: { installReferrer?: string } | null, error?: unknown) => void
|
|
45
45
|
) => void;
|
|
46
|
-
}
|
|
46
|
+
};
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
let playInstallReferrer: PlayInstallReferrerModule | null | undefined;
|
|
49
|
+
|
|
50
|
+
function getPlayInstallReferrer(): PlayInstallReferrerModule | null {
|
|
51
|
+
if (playInstallReferrer !== undefined) return playInstallReferrer;
|
|
52
|
+
try {
|
|
53
|
+
playInstallReferrer = require("react-native-play-install-referrer")
|
|
54
|
+
.PlayInstallReferrer ?? null;
|
|
55
|
+
} catch {
|
|
56
|
+
playInstallReferrer = null;
|
|
57
|
+
}
|
|
58
|
+
return playInstallReferrer ?? null;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -71,6 +77,7 @@ const INSTALL_REFERRER_TIMEOUT_MS = 1500;
|
|
|
71
77
|
export interface CaptureOptions {
|
|
72
78
|
customRefParams?: string[];
|
|
73
79
|
pathPattern?: string;
|
|
80
|
+
canCapture?: () => boolean;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
/**
|
|
@@ -81,6 +88,7 @@ export async function captureInstallReferrer(
|
|
|
81
88
|
options: CaptureOptions = {}
|
|
82
89
|
): Promise<void> {
|
|
83
90
|
try {
|
|
91
|
+
if (options.canCapture && !options.canCapture()) return;
|
|
84
92
|
// The one-shot flag is only useful if it can persist across launches.
|
|
85
93
|
// Without AsyncStorage (MemoryStorage fallback) the flag is lost every
|
|
86
94
|
// restart, so we'd re-hit the native API every cold start. Mirror the
|
|
@@ -114,7 +122,7 @@ export async function captureInstallReferrer(
|
|
|
114
122
|
return;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
if (didResolve) {
|
|
125
|
+
if (didResolve && (!options.canCapture || options.canCapture())) {
|
|
118
126
|
await storage().setAsync(LOCAL_INSTALL_REFERRER_RESOLVED_KEY, "true");
|
|
119
127
|
}
|
|
120
128
|
} catch (error) {
|
|
@@ -130,7 +138,8 @@ export async function captureInstallReferrer(
|
|
|
130
138
|
async function captureAndroidReferrer(
|
|
131
139
|
options: CaptureOptions
|
|
132
140
|
): Promise<boolean> {
|
|
133
|
-
|
|
141
|
+
const referrer = getPlayInstallReferrer();
|
|
142
|
+
if (!referrer) {
|
|
134
143
|
// Warn (not debug) on Android: attribution silently does nothing here, and
|
|
135
144
|
// marking the peer optional suppresses the missing-peer install warning, so
|
|
136
145
|
// this is the only actionable signal the integrator gets.
|
|
@@ -174,7 +183,7 @@ async function captureAndroidReferrer(
|
|
|
174
183
|
}, INSTALL_REFERRER_TIMEOUT_MS);
|
|
175
184
|
|
|
176
185
|
try {
|
|
177
|
-
|
|
186
|
+
referrer.getInstallReferrerInfo((info, error) => {
|
|
178
187
|
if (error) {
|
|
179
188
|
logger.debug("InstallReferrer: Play API error", error);
|
|
180
189
|
finish({ ok: false, info: null });
|
|
@@ -189,6 +198,7 @@ async function captureAndroidReferrer(
|
|
|
189
198
|
});
|
|
190
199
|
|
|
191
200
|
if (!result.ok) return false; // errored — retry next launch
|
|
201
|
+
if (options.canCapture && !options.canCapture()) return false;
|
|
192
202
|
|
|
193
203
|
const referrerQuery = result.info?.installReferrer;
|
|
194
204
|
if (!referrerQuery) {
|
|
@@ -46,6 +46,7 @@ interface IFormoAnalyticsInstance {
|
|
|
46
46
|
isAutocaptureEnabled(
|
|
47
47
|
eventType: "connect" | "disconnect" | "signature" | "transaction" | "chain"
|
|
48
48
|
): boolean;
|
|
49
|
+
hasOptedOutTracking(): boolean;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export class WagmiEventHandler {
|
|
@@ -121,6 +122,12 @@ export class WagmiEventHandler {
|
|
|
121
122
|
status: WagmiState["status"],
|
|
122
123
|
prevStatus: WagmiState["status"]
|
|
123
124
|
): Promise<void> {
|
|
125
|
+
if (this.formo.hasOptedOutTracking()) {
|
|
126
|
+
this.clearIdentity();
|
|
127
|
+
this.trackingState.lastStatus = status;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
if (this.trackingState.isProcessing) {
|
|
125
132
|
// Limit queue size to prevent unbounded growth during rapid status changes
|
|
126
133
|
if (this.pendingStatusChanges.length >= WagmiEventHandler.MAX_PENDING_STATUS_CHANGES) {
|
|
@@ -163,6 +170,12 @@ export class WagmiEventHandler {
|
|
|
163
170
|
const address = this.getConnectedAddress(state);
|
|
164
171
|
const chainId = state.chainId;
|
|
165
172
|
|
|
173
|
+
if (this.formo.hasOptedOutTracking()) {
|
|
174
|
+
this.clearIdentity();
|
|
175
|
+
this.trackingState.lastStatus = status;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
logger.info("WagmiEventHandler: Status changed", {
|
|
167
180
|
status,
|
|
168
181
|
prevStatus,
|
|
@@ -216,6 +229,11 @@ export class WagmiEventHandler {
|
|
|
216
229
|
chainId: number | undefined,
|
|
217
230
|
prevChainId: number | undefined
|
|
218
231
|
): Promise<void> {
|
|
232
|
+
if (this.formo.hasOptedOutTracking()) {
|
|
233
|
+
this.clearIdentity();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
219
237
|
// Skip if no change, chainId is undefined, or this is initial connection (prevChainId undefined)
|
|
220
238
|
if (chainId === prevChainId || chainId === undefined || prevChainId === undefined) {
|
|
221
239
|
return;
|
|
@@ -548,6 +566,12 @@ export class WagmiEventHandler {
|
|
|
548
566
|
return connection?.connector.id;
|
|
549
567
|
}
|
|
550
568
|
|
|
569
|
+
public clearIdentity(): void {
|
|
570
|
+
this.trackingState.lastAddress = undefined;
|
|
571
|
+
this.trackingState.lastChainId = undefined;
|
|
572
|
+
this.pendingStatusChanges = [];
|
|
573
|
+
}
|
|
574
|
+
|
|
551
575
|
/**
|
|
552
576
|
* Clean up subscriptions
|
|
553
577
|
*/
|
package/src/version.ts
CHANGED