@formo/analytics-react-native 1.1.1 → 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 +64 -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 +65 -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 +88 -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;
|
|
@@ -115,10 +118,15 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
// Initialize event manager
|
|
118
|
-
this.eventManager = new EventManager(
|
|
121
|
+
this.eventManager = new EventManager(
|
|
122
|
+
this.eventQueue,
|
|
123
|
+
options,
|
|
124
|
+
() => !this.hasOptedOutTracking()
|
|
125
|
+
);
|
|
119
126
|
|
|
120
127
|
// Check consent status
|
|
121
128
|
if (this.hasOptedOutTracking()) {
|
|
129
|
+
clearTrafficSource();
|
|
122
130
|
logger.info("User has previously opted out of tracking");
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -183,9 +191,13 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
183
191
|
|
|
184
192
|
if (analytics.isAttributionEnabled("installReferrer")) {
|
|
185
193
|
try {
|
|
194
|
+
const consentGeneration = analytics.consentGeneration;
|
|
186
195
|
await captureInstallReferrer({
|
|
187
196
|
customRefParams: analytics.options.referral?.queryParams,
|
|
188
197
|
pathPattern: analytics.options.referral?.pathPattern,
|
|
198
|
+
canCapture: () =>
|
|
199
|
+
analytics.consentGeneration === consentGeneration &&
|
|
200
|
+
!analytics.hasOptedOutTracking(),
|
|
189
201
|
});
|
|
190
202
|
} catch (error) {
|
|
191
203
|
logger.debug("FormoAnalytics: install referrer capture failed", error);
|
|
@@ -237,9 +249,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
237
249
|
* so attribution is in storage before the first lifecycle event fires.
|
|
238
250
|
*/
|
|
239
251
|
private async startDeepLinkCapture(): Promise<void> {
|
|
252
|
+
const consentGeneration = this.consentGeneration;
|
|
240
253
|
try {
|
|
241
254
|
const url = await Linking.getInitialURL();
|
|
242
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
url &&
|
|
257
|
+
this.consentGeneration === consentGeneration &&
|
|
258
|
+
!this.hasOptedOutTracking()
|
|
259
|
+
) {
|
|
243
260
|
if (this.isAttributionEnabled("deeplinks")) {
|
|
244
261
|
this.setTrafficSourceFromUrl(url);
|
|
245
262
|
}
|
|
@@ -254,7 +271,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
254
271
|
|
|
255
272
|
// Runtime deep links (foreground opens, universal links).
|
|
256
273
|
this.linkingSubscription = Linking.addEventListener("url", (event) => {
|
|
257
|
-
if (!event?.url) return;
|
|
274
|
+
if (!event?.url || this.hasOptedOutTracking()) return;
|
|
258
275
|
// Each behaviour checks its own flag: the hook may exist because only one
|
|
259
276
|
// of them is enabled.
|
|
260
277
|
if (this.isAttributionEnabled("deeplinks")) {
|
|
@@ -378,6 +395,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
378
395
|
* ```
|
|
379
396
|
*/
|
|
380
397
|
public setTrafficSourceFromUrl(url: string): void {
|
|
398
|
+
if (this.hasOptedOutTracking()) return;
|
|
381
399
|
const trafficSource = parseTrafficSource(
|
|
382
400
|
url,
|
|
383
401
|
this.options.referral?.queryParams,
|
|
@@ -390,12 +408,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
390
408
|
logger.debug("Traffic source set from URL:", trafficSource);
|
|
391
409
|
}
|
|
392
410
|
|
|
393
|
-
/**
|
|
394
|
-
* Reset the current user session
|
|
395
|
-
*/
|
|
411
|
+
/** Reset user and wallet state, preserving device identity and attribution. */
|
|
396
412
|
public reset(): void {
|
|
413
|
+
this.walletGeneration++;
|
|
414
|
+
this.chainGeneration++;
|
|
397
415
|
this.currentUserId = undefined;
|
|
398
|
-
|
|
416
|
+
this.currentAddress = undefined;
|
|
417
|
+
this.currentChainId = undefined;
|
|
418
|
+
this.wagmiHandler?.clearIdentity();
|
|
399
419
|
storage().remove(LOCAL_SESSION_ID_KEY);
|
|
400
420
|
storage().remove(LOCAL_SESSION_LAST_ACTIVITY_KEY);
|
|
401
421
|
storage().remove(SESSION_USER_ID_KEY);
|
|
@@ -458,6 +478,9 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
458
478
|
logger.warn(`Connect: Invalid address provided ("${address}")`);
|
|
459
479
|
return;
|
|
460
480
|
}
|
|
481
|
+
if (this.hasOptedOutTracking()) return;
|
|
482
|
+
const walletGeneration = ++this.walletGeneration;
|
|
483
|
+
const chainGeneration = ++this.chainGeneration;
|
|
461
484
|
|
|
462
485
|
// Track event before updating state so connect events TO excluded chains are tracked
|
|
463
486
|
await this.trackEvent(
|
|
@@ -468,8 +491,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
468
491
|
callback
|
|
469
492
|
);
|
|
470
493
|
|
|
471
|
-
this.
|
|
472
|
-
|
|
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
|
+
}
|
|
473
502
|
}
|
|
474
503
|
|
|
475
504
|
/**
|
|
@@ -481,6 +510,8 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
481
510
|
context?: IFormoEventContext,
|
|
482
511
|
callback?: (...args: unknown[]) => void
|
|
483
512
|
): Promise<void> {
|
|
513
|
+
const walletGeneration = ++this.walletGeneration;
|
|
514
|
+
const chainGeneration = ++this.chainGeneration;
|
|
484
515
|
const chainId = params?.chainId || this.currentChainId;
|
|
485
516
|
const address = params?.address || this.currentAddress;
|
|
486
517
|
|
|
@@ -500,8 +531,8 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
500
531
|
callback
|
|
501
532
|
);
|
|
502
533
|
|
|
503
|
-
this.currentAddress = undefined;
|
|
504
|
-
this.currentChainId = undefined;
|
|
534
|
+
if (walletGeneration === this.walletGeneration) this.currentAddress = undefined;
|
|
535
|
+
if (chainGeneration === this.chainGeneration) this.currentChainId = undefined;
|
|
505
536
|
}
|
|
506
537
|
|
|
507
538
|
/**
|
|
@@ -521,22 +552,37 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
521
552
|
logger.warn("FormoAnalytics::chain: chainId must be a valid number");
|
|
522
553
|
return;
|
|
523
554
|
}
|
|
524
|
-
|
|
525
|
-
|
|
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");
|
|
526
560
|
return;
|
|
527
561
|
}
|
|
562
|
+
if (this.hasOptedOutTracking()) return;
|
|
563
|
+
const walletGeneration = address
|
|
564
|
+
? ++this.walletGeneration
|
|
565
|
+
: this.walletGeneration;
|
|
566
|
+
const chainGeneration = ++this.chainGeneration;
|
|
528
567
|
|
|
529
568
|
// Track event before updating currentChainId so shouldTrack uses the previous chain
|
|
530
569
|
// This ensures chain change events TO excluded chains are still tracked
|
|
531
570
|
await this.trackEvent(
|
|
532
571
|
EventType.CHAIN,
|
|
533
|
-
{ chainId, address:
|
|
572
|
+
{ chainId, address: validAddress },
|
|
534
573
|
properties,
|
|
535
574
|
context,
|
|
536
575
|
callback
|
|
537
576
|
);
|
|
538
577
|
|
|
539
|
-
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
|
+
}
|
|
540
586
|
}
|
|
541
587
|
|
|
542
588
|
/**
|
|
@@ -649,6 +695,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
649
695
|
try {
|
|
650
696
|
const { userId, address, providerName, rdns } = params;
|
|
651
697
|
logger.info("Identify", address, userId, providerName, rdns);
|
|
698
|
+
if (this.hasOptedOutTracking()) return;
|
|
652
699
|
|
|
653
700
|
let validAddress: Address | undefined = undefined;
|
|
654
701
|
if (address) {
|
|
@@ -657,10 +704,12 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
657
704
|
logger.warn(`Identify: Invalid address provided ("${address}")`);
|
|
658
705
|
return;
|
|
659
706
|
}
|
|
707
|
+
this.walletGeneration++;
|
|
660
708
|
this.currentAddress = validAddress;
|
|
661
709
|
// Note: validateAddress returns Solana addresses unchanged (Base58, case-sensitive)
|
|
662
710
|
// and EVM addresses checksummed.
|
|
663
711
|
} else {
|
|
712
|
+
this.walletGeneration++;
|
|
664
713
|
this.currentAddress = undefined;
|
|
665
714
|
}
|
|
666
715
|
|
|
@@ -707,6 +756,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
707
756
|
context?: IFormoEventContext,
|
|
708
757
|
callback?: (...args: unknown[]) => void
|
|
709
758
|
): Promise<void> {
|
|
759
|
+
if (this.hasOptedOutTracking()) return;
|
|
710
760
|
if (this.session.isWalletDetected(rdns)) {
|
|
711
761
|
logger.warn(`Detect: Wallet ${providerName} already detected in this session`);
|
|
712
762
|
return;
|
|
@@ -745,9 +795,14 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
745
795
|
*/
|
|
746
796
|
public optOutTracking(): void {
|
|
747
797
|
logger.info("Opting out of tracking");
|
|
798
|
+
this.consentGeneration++;
|
|
799
|
+
this.initialDeepLinkUrl = undefined;
|
|
748
800
|
setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
|
|
749
801
|
this.eventQueue.clear();
|
|
750
802
|
this.reset();
|
|
803
|
+
// Consent withdrawal clears device identity and attribution too.
|
|
804
|
+
storage().remove(LOCAL_ANONYMOUS_ID_KEY);
|
|
805
|
+
clearTrafficSource();
|
|
751
806
|
logger.info("Successfully opted out of tracking");
|
|
752
807
|
}
|
|
753
808
|
|
|
@@ -853,7 +908,18 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
853
908
|
callback?: (...args: unknown[]) => void
|
|
854
909
|
): Promise<void> {
|
|
855
910
|
try {
|
|
856
|
-
|
|
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)) {
|
|
857
923
|
logger.info(`Skipping ${type} event due to tracking configuration`);
|
|
858
924
|
return;
|
|
859
925
|
}
|
|
@@ -884,7 +950,7 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
884
950
|
/**
|
|
885
951
|
* Check if tracking should be enabled
|
|
886
952
|
*/
|
|
887
|
-
private shouldTrack(): boolean {
|
|
953
|
+
private shouldTrack(eventChainId?: ChainID): boolean {
|
|
888
954
|
// Check consent
|
|
889
955
|
if (this.hasOptedOutTracking()) {
|
|
890
956
|
return false;
|
|
@@ -902,11 +968,13 @@ export class FormoAnalytics implements IFormoAnalytics {
|
|
|
902
968
|
!Array.isArray(this.options.tracking)
|
|
903
969
|
) {
|
|
904
970
|
const { excludeChains = [] } = this.options.tracking as TrackingOptions;
|
|
971
|
+
const chainId = eventChainId ?? this.currentChainId;
|
|
905
972
|
|
|
906
973
|
if (
|
|
907
974
|
excludeChains.length > 0 &&
|
|
908
|
-
|
|
909
|
-
|
|
975
|
+
chainId !== undefined &&
|
|
976
|
+
chainId !== null &&
|
|
977
|
+
excludeChains.includes(chainId)
|
|
910
978
|
) {
|
|
911
979
|
return false;
|
|
912
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