@formo/analytics 1.35.2 → 1.37.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/dist/cjs/src/FormoAnalytics.d.ts +79 -363
- package/dist/cjs/src/FormoAnalytics.js +368 -2234
- package/dist/cjs/src/event/EventManager.d.ts +3 -1
- package/dist/cjs/src/event/EventManager.js +5 -1
- package/dist/cjs/src/event/type.d.ts +1 -0
- package/dist/cjs/src/evm/EvmEventTracker.d.ts +175 -0
- package/dist/cjs/src/evm/EvmEventTracker.js +1030 -0
- package/dist/cjs/src/evm/EvmProviderRegistry.d.ts +132 -0
- package/dist/cjs/src/evm/EvmProviderRegistry.js +348 -0
- package/dist/cjs/src/evm/EvmRequestTracker.d.ts +98 -0
- package/dist/cjs/src/evm/EvmRequestTracker.js +713 -0
- package/dist/cjs/src/evm/batch.d.ts +94 -0
- package/dist/cjs/src/evm/batch.js +130 -0
- package/dist/cjs/src/queue/EventQueue.d.ts +28 -0
- package/dist/cjs/src/queue/EventQueue.js +97 -19
- package/dist/cjs/src/queue/type.d.ts +1 -0
- package/dist/cjs/src/tracking/TrackingPolicy.d.ts +146 -0
- package/dist/cjs/src/tracking/TrackingPolicy.js +200 -0
- package/dist/cjs/src/validators/address.d.ts +1 -1
- package/dist/cjs/src/version.d.ts +1 -1
- package/dist/cjs/src/version.js +1 -1
- package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +43 -0
- package/dist/cjs/src/wagmi/WagmiEventHandler.js +253 -2
- package/dist/cjs/src/wallet/WalletStateStore.d.ts +223 -0
- package/dist/cjs/src/wallet/WalletStateStore.js +515 -0
- package/dist/esm/src/FormoAnalytics.d.ts +79 -363
- package/dist/esm/src/FormoAnalytics.js +369 -2235
- package/dist/esm/src/event/EventManager.d.ts +3 -1
- package/dist/esm/src/event/EventManager.js +5 -1
- package/dist/esm/src/event/type.d.ts +1 -0
- package/dist/esm/src/evm/EvmEventTracker.d.ts +175 -0
- package/dist/esm/src/evm/EvmEventTracker.js +1027 -0
- package/dist/esm/src/evm/EvmProviderRegistry.d.ts +132 -0
- package/dist/esm/src/evm/EvmProviderRegistry.js +345 -0
- package/dist/esm/src/evm/EvmRequestTracker.d.ts +98 -0
- package/dist/esm/src/evm/EvmRequestTracker.js +710 -0
- package/dist/esm/src/evm/batch.d.ts +94 -0
- package/dist/esm/src/evm/batch.js +123 -0
- package/dist/esm/src/queue/EventQueue.d.ts +28 -0
- package/dist/esm/src/queue/EventQueue.js +97 -19
- package/dist/esm/src/queue/type.d.ts +1 -0
- package/dist/esm/src/tracking/TrackingPolicy.d.ts +146 -0
- package/dist/esm/src/tracking/TrackingPolicy.js +197 -0
- package/dist/esm/src/version.d.ts +1 -1
- package/dist/esm/src/version.js +1 -1
- package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +43 -0
- package/dist/esm/src/wagmi/WagmiEventHandler.js +253 -2
- package/dist/esm/src/wallet/WalletStateStore.d.ts +223 -0
- package/dist/esm/src/wallet/WalletStateStore.js +512 -0
- package/dist/index.umd.min.js +1 -1
- package/package.json +6 -4
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { TransactionStatus } from "../types/events";
|
|
2
|
+
/**
|
|
3
|
+
* EIP-5792 batch settlement, shared by both capture paths.
|
|
4
|
+
*
|
|
5
|
+
* The EIP-1193 request wrapper (`EvmRequestTracker`) and the wagmi cache
|
|
6
|
+
* observer (`WagmiEventHandler`) each see a batch through a different
|
|
7
|
+
* transport, but a settled batch means the same thing in both. Keeping the
|
|
8
|
+
* outcome rules in one place is what stops the two paths from drifting into
|
|
9
|
+
* reporting the same batch differently depending on how the app happened to
|
|
10
|
+
* integrate the SDK.
|
|
11
|
+
*/
|
|
12
|
+
/** A settled batch as `wallet_getCallsStatus` (or viem's wrapper) reports it. */
|
|
13
|
+
export type BatchStatusResult = {
|
|
14
|
+
status?: number | string;
|
|
15
|
+
statusCode?: number;
|
|
16
|
+
atomic?: boolean;
|
|
17
|
+
chainId?: number | string;
|
|
18
|
+
receipts?: BatchReceipt[];
|
|
19
|
+
} | null | undefined;
|
|
20
|
+
export type BatchReceipt = {
|
|
21
|
+
status?: string | number;
|
|
22
|
+
transactionHash?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The batch identifier from a `wallet_sendCalls` result.
|
|
26
|
+
*
|
|
27
|
+
* EIP-5792 settled on `{ id }`, but wallets shipped against the earlier
|
|
28
|
+
* draft return a bare string. Both are accepted so a wallet on either
|
|
29
|
+
* version is still grouped.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readBatchId(result: unknown): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The numeric EIP-5792 status code from a settlement result.
|
|
34
|
+
*
|
|
35
|
+
* A wallet answers `wallet_getCallsStatus` with a numeric `status`; viem's
|
|
36
|
+
* `getCallsStatus` renames that to `statusCode` and puts a summary string in
|
|
37
|
+
* `status` instead. Both shapes arrive here depending on the capture path,
|
|
38
|
+
* so read the number wherever it is and never trust the string.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readBatchStatusCode(res: BatchStatusResult): number | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The chain a settled batch reports itself on.
|
|
43
|
+
*
|
|
44
|
+
* EIP-5792 v2 puts `chainId` in the `wallet_getCallsStatus` response as hex;
|
|
45
|
+
* viem returns it as a number. Either way it names the chain the batch
|
|
46
|
+
* actually settled on, which outranks a chain merely inferred from the
|
|
47
|
+
* connection at broadcast time - the wallet can move chains while the
|
|
48
|
+
* prompt is up.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readBatchChainId(res: BatchStatusResult): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How one call in a settled batch ended.
|
|
53
|
+
*
|
|
54
|
+
* A per-call receipt is authoritative where it exists: that is what makes a
|
|
55
|
+
* partially reverted non-atomic batch report honestly rather than tarring
|
|
56
|
+
* every call with the batch's worst outcome. A receipt whose own status is
|
|
57
|
+
* unreadable falls back to the batch verdict rather than being assumed good.
|
|
58
|
+
*
|
|
59
|
+
* Receipt statuses come in two spellings: raw RPC (`"0x0"`/`"0x1"`, or the
|
|
60
|
+
* numbers) and viem-formatted (`"reverted"`/`"success"`), because the wagmi
|
|
61
|
+
* path sees receipts after viem has normalised them.
|
|
62
|
+
*
|
|
63
|
+
* The codes are EIP-5792's: 200 confirmed, 400 failed BEFORE landing on
|
|
64
|
+
* chain, 500 reverted, 600 partially reverted. 400 is a rejection, not a
|
|
65
|
+
* revert - nothing was mined, so calling it reverted would misreport gas
|
|
66
|
+
* spent and on-chain activity that never happened.
|
|
67
|
+
*
|
|
68
|
+
* Returns undefined when the call cannot be decided, which happens on 600
|
|
69
|
+
* for a call the wallet gave no receipt for.
|
|
70
|
+
*/
|
|
71
|
+
export declare function batchCallOutcome(code: number, receipt?: BatchReceipt): TransactionStatus | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The receipt that decides call `index`, honouring atomic execution.
|
|
74
|
+
*
|
|
75
|
+
* An atomic batch lands as ONE on-chain transaction, so the wallet returns a
|
|
76
|
+
* single receipt covering every call. Indexing receipts positionally there
|
|
77
|
+
* would hand the shared hash to call 0 and leave its siblings hashless and
|
|
78
|
+
* decided only by the batch verdict. Every call in an atomic batch shares
|
|
79
|
+
* the one receipt - same hash, same fate - which is also what makes
|
|
80
|
+
* `count(distinct transaction_hash)` count on-chain transactions correctly.
|
|
81
|
+
*
|
|
82
|
+
* The wallet's own `atomic` flag is authoritative in BOTH directions. An
|
|
83
|
+
* explicit `atomic: false` with a single receipt is a real shape - a
|
|
84
|
+
* non-atomic batch whose execution stopped after one call mined - and
|
|
85
|
+
* sharing that receipt would hand calls that never reached the chain a
|
|
86
|
+
* transaction hash they do not have. Only when the field is ABSENT (a
|
|
87
|
+
* wallet predating it, reached over raw EIP-1193; viem fills the field in,
|
|
88
|
+
* so the wagmi path never lands here) does the conservative inference
|
|
89
|
+
* apply: one receipt for several calls on a batch that is NOT partially
|
|
90
|
+
* reverted can only be atomic execution (600 explicitly means some calls
|
|
91
|
+
* reverted and others did not, which one shared transaction cannot do).
|
|
92
|
+
*/
|
|
93
|
+
export declare function batchReceiptForCall(res: BatchStatusResult, index: number, callCount: number): BatchReceipt | undefined;
|
|
94
|
+
//# sourceMappingURL=batch.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { TransactionStatus } from "../types/events";
|
|
2
|
+
/**
|
|
3
|
+
* The batch identifier from a `wallet_sendCalls` result.
|
|
4
|
+
*
|
|
5
|
+
* EIP-5792 settled on `{ id }`, but wallets shipped against the earlier
|
|
6
|
+
* draft return a bare string. Both are accepted so a wallet on either
|
|
7
|
+
* version is still grouped.
|
|
8
|
+
*/
|
|
9
|
+
export function readBatchId(result) {
|
|
10
|
+
if (typeof result === "string" && result.length > 0)
|
|
11
|
+
return result;
|
|
12
|
+
if (result && typeof result === "object") {
|
|
13
|
+
var id = result.id;
|
|
14
|
+
if (typeof id === "string" && id.length > 0)
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The numeric EIP-5792 status code from a settlement result.
|
|
21
|
+
*
|
|
22
|
+
* A wallet answers `wallet_getCallsStatus` with a numeric `status`; viem's
|
|
23
|
+
* `getCallsStatus` renames that to `statusCode` and puts a summary string in
|
|
24
|
+
* `status` instead. Both shapes arrive here depending on the capture path,
|
|
25
|
+
* so read the number wherever it is and never trust the string.
|
|
26
|
+
*/
|
|
27
|
+
export function readBatchStatusCode(res) {
|
|
28
|
+
if (typeof (res === null || res === void 0 ? void 0 : res.statusCode) === "number")
|
|
29
|
+
return res.statusCode;
|
|
30
|
+
if (typeof (res === null || res === void 0 ? void 0 : res.status) === "number")
|
|
31
|
+
return res.status;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The chain a settled batch reports itself on.
|
|
36
|
+
*
|
|
37
|
+
* EIP-5792 v2 puts `chainId` in the `wallet_getCallsStatus` response as hex;
|
|
38
|
+
* viem returns it as a number. Either way it names the chain the batch
|
|
39
|
+
* actually settled on, which outranks a chain merely inferred from the
|
|
40
|
+
* connection at broadcast time - the wallet can move chains while the
|
|
41
|
+
* prompt is up.
|
|
42
|
+
*/
|
|
43
|
+
export function readBatchChainId(res) {
|
|
44
|
+
var raw = res === null || res === void 0 ? void 0 : res.chainId;
|
|
45
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
|
|
46
|
+
return raw;
|
|
47
|
+
if (typeof raw === "string") {
|
|
48
|
+
var parsed = parseInt(raw, 16);
|
|
49
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* How one call in a settled batch ended.
|
|
56
|
+
*
|
|
57
|
+
* A per-call receipt is authoritative where it exists: that is what makes a
|
|
58
|
+
* partially reverted non-atomic batch report honestly rather than tarring
|
|
59
|
+
* every call with the batch's worst outcome. A receipt whose own status is
|
|
60
|
+
* unreadable falls back to the batch verdict rather than being assumed good.
|
|
61
|
+
*
|
|
62
|
+
* Receipt statuses come in two spellings: raw RPC (`"0x0"`/`"0x1"`, or the
|
|
63
|
+
* numbers) and viem-formatted (`"reverted"`/`"success"`), because the wagmi
|
|
64
|
+
* path sees receipts after viem has normalised them.
|
|
65
|
+
*
|
|
66
|
+
* The codes are EIP-5792's: 200 confirmed, 400 failed BEFORE landing on
|
|
67
|
+
* chain, 500 reverted, 600 partially reverted. 400 is a rejection, not a
|
|
68
|
+
* revert - nothing was mined, so calling it reverted would misreport gas
|
|
69
|
+
* spent and on-chain activity that never happened.
|
|
70
|
+
*
|
|
71
|
+
* Returns undefined when the call cannot be decided, which happens on 600
|
|
72
|
+
* for a call the wallet gave no receipt for.
|
|
73
|
+
*/
|
|
74
|
+
export function batchCallOutcome(code, receipt) {
|
|
75
|
+
var receiptStatus = receipt === null || receipt === void 0 ? void 0 : receipt.status;
|
|
76
|
+
if (receiptStatus !== undefined) {
|
|
77
|
+
return receiptStatus === "0x0" ||
|
|
78
|
+
receiptStatus === 0 ||
|
|
79
|
+
receiptStatus === "reverted"
|
|
80
|
+
? TransactionStatus.REVERTED
|
|
81
|
+
: TransactionStatus.CONFIRMED;
|
|
82
|
+
}
|
|
83
|
+
if (code >= 600)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (code >= 500)
|
|
86
|
+
return TransactionStatus.REVERTED;
|
|
87
|
+
if (code >= 400)
|
|
88
|
+
return TransactionStatus.REJECTED;
|
|
89
|
+
return TransactionStatus.CONFIRMED;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The receipt that decides call `index`, honouring atomic execution.
|
|
93
|
+
*
|
|
94
|
+
* An atomic batch lands as ONE on-chain transaction, so the wallet returns a
|
|
95
|
+
* single receipt covering every call. Indexing receipts positionally there
|
|
96
|
+
* would hand the shared hash to call 0 and leave its siblings hashless and
|
|
97
|
+
* decided only by the batch verdict. Every call in an atomic batch shares
|
|
98
|
+
* the one receipt - same hash, same fate - which is also what makes
|
|
99
|
+
* `count(distinct transaction_hash)` count on-chain transactions correctly.
|
|
100
|
+
*
|
|
101
|
+
* The wallet's own `atomic` flag is authoritative in BOTH directions. An
|
|
102
|
+
* explicit `atomic: false` with a single receipt is a real shape - a
|
|
103
|
+
* non-atomic batch whose execution stopped after one call mined - and
|
|
104
|
+
* sharing that receipt would hand calls that never reached the chain a
|
|
105
|
+
* transaction hash they do not have. Only when the field is ABSENT (a
|
|
106
|
+
* wallet predating it, reached over raw EIP-1193; viem fills the field in,
|
|
107
|
+
* so the wagmi path never lands here) does the conservative inference
|
|
108
|
+
* apply: one receipt for several calls on a batch that is NOT partially
|
|
109
|
+
* reverted can only be atomic execution (600 explicitly means some calls
|
|
110
|
+
* reverted and others did not, which one shared transaction cannot do).
|
|
111
|
+
*/
|
|
112
|
+
export function batchReceiptForCall(res, index, callCount) {
|
|
113
|
+
var _a;
|
|
114
|
+
var receipts = Array.isArray(res === null || res === void 0 ? void 0 : res.receipts) ? res.receipts : [];
|
|
115
|
+
var code = (_a = readBatchStatusCode(res)) !== null && _a !== void 0 ? _a : 0;
|
|
116
|
+
var atomic = (res === null || res === void 0 ? void 0 : res.atomic) === true ||
|
|
117
|
+
((res === null || res === void 0 ? void 0 : res.atomic) === undefined &&
|
|
118
|
+
receipts.length === 1 &&
|
|
119
|
+
callCount > 1 &&
|
|
120
|
+
code < 600);
|
|
121
|
+
return atomic ? receipts[0] : receipts[index];
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=batch.js.map
|
|
@@ -28,6 +28,8 @@ export declare class EventQueue implements IEventQueue {
|
|
|
28
28
|
private pendingFlush;
|
|
29
29
|
private payloadHashes;
|
|
30
30
|
private canSend?;
|
|
31
|
+
private closed;
|
|
32
|
+
private disposePageLeave;
|
|
31
33
|
constructor(writeKey: string, options: Options);
|
|
32
34
|
private generateMessageId;
|
|
33
35
|
/**
|
|
@@ -35,6 +37,26 @@ export declare class EventQueue implements IEventQueue {
|
|
|
35
37
|
* withdrawal / SDK teardown so nothing buffered can be sent later.
|
|
36
38
|
*/
|
|
37
39
|
clear(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Terminal shutdown. Unlike clear(), which only empties the buffer and can
|
|
42
|
+
* be followed by more events (consent is re-granted, say), close() makes
|
|
43
|
+
* this queue permanently inert: every later enqueue() is a no-op and the
|
|
44
|
+
* page-leave listeners are removed. flush() needs no guard of its own,
|
|
45
|
+
* because a closed queue starts empty and can never be filled again.
|
|
46
|
+
*
|
|
47
|
+
* This is the guarantee a torn-down SDK instance needs. Emission decisions
|
|
48
|
+
* and the emission itself are separated by an await in many call paths
|
|
49
|
+
* (event creation is async), so guarding the *caller* cannot work: the
|
|
50
|
+
* instance may be destroyed while the continuation is in flight. Enforcing
|
|
51
|
+
* it here means no holder of a stale reference can ever send.
|
|
52
|
+
*
|
|
53
|
+
* What close() deliberately does NOT stop is a flush already in flight.
|
|
54
|
+
* Those events were accepted while the instance was alive, so they are
|
|
55
|
+
* real data; abandoning them would turn every unmount into silent loss.
|
|
56
|
+
*/
|
|
57
|
+
close(): void;
|
|
58
|
+
/** True once close() has run. Exposed for teardown assertions. */
|
|
59
|
+
get isClosed(): boolean;
|
|
38
60
|
enqueue(event: IFormoEvent, callback?: (...args: any) => void): Promise<void>;
|
|
39
61
|
flush(callback?: (...args: any) => void, drainAll?: boolean): Promise<void | IFormoEventFlushPayload[]>;
|
|
40
62
|
/**
|
|
@@ -58,6 +80,12 @@ export declare class EventQueue implements IEventQueue {
|
|
|
58
80
|
private sendBatches;
|
|
59
81
|
private isErrorRetryable;
|
|
60
82
|
private isDuplicate;
|
|
83
|
+
/**
|
|
84
|
+
* Installs the page-leave listeners and returns a disposer that removes
|
|
85
|
+
* every one of them. Without the disposer each SDK instance leaked five
|
|
86
|
+
* listeners (three on the global, two on the document) that kept the
|
|
87
|
+
* instance and its queue alive for the life of the page.
|
|
88
|
+
*/
|
|
61
89
|
private onPageLeave;
|
|
62
90
|
}
|
|
63
91
|
export {};
|
|
@@ -83,11 +83,23 @@ var EventQueue = /** @class */ (function () {
|
|
|
83
83
|
this.queue = [];
|
|
84
84
|
this.queueByteSize = 0; // running total of queued items' byteSize
|
|
85
85
|
this.payloadHashes = new Set();
|
|
86
|
+
// Terminal shutdown flag. Once set, enqueue() and flush() are no-ops for
|
|
87
|
+
// the rest of this instance's life. See close().
|
|
88
|
+
this.closed = false;
|
|
89
|
+
// Undoes the page-leave listeners installed in the constructor.
|
|
90
|
+
this.disposePageLeave = null;
|
|
91
|
+
/**
|
|
92
|
+
* Installs the page-leave listeners and returns a disposer that removes
|
|
93
|
+
* every one of them. Without the disposer each SDK instance leaked five
|
|
94
|
+
* listeners (three on the global, two on the document) that kept the
|
|
95
|
+
* instance and its queue alive for the life of the page.
|
|
96
|
+
*/
|
|
86
97
|
this.onPageLeave = function (callback) {
|
|
87
98
|
// To ensure the callback is only called once even if more than one events
|
|
88
99
|
// are fired at once.
|
|
89
100
|
var pageLeft = false;
|
|
90
101
|
var isAccessible = false;
|
|
102
|
+
var resetTimer = null;
|
|
91
103
|
function handleOnLeave() {
|
|
92
104
|
if (pageLeft) {
|
|
93
105
|
return;
|
|
@@ -97,33 +109,27 @@ var EventQueue = /** @class */ (function () {
|
|
|
97
109
|
// Reset pageLeft on the next tick
|
|
98
110
|
// to ensure callback executes for other listeners
|
|
99
111
|
// when closing an inactive browser tab.
|
|
100
|
-
setTimeout(function () {
|
|
112
|
+
resetTimer = setTimeout(function () {
|
|
113
|
+
resetTimer = null;
|
|
101
114
|
pageLeft = false;
|
|
102
115
|
}, 0);
|
|
103
116
|
}
|
|
104
|
-
|
|
105
|
-
// Includes user actions like clicking a link, entering a new URL,
|
|
106
|
-
// refreshing the page, or closing the browser tab
|
|
107
|
-
// Note that 'pagehide' is not supported in IE.
|
|
108
|
-
// So, this is a fallback.
|
|
109
|
-
globalThis.addEventListener("beforeunload", function () {
|
|
117
|
+
var onBeforeUnload = function () {
|
|
110
118
|
isAccessible = false;
|
|
111
119
|
handleOnLeave();
|
|
112
|
-
}
|
|
113
|
-
|
|
120
|
+
};
|
|
121
|
+
var onBlur = function () {
|
|
114
122
|
isAccessible = true;
|
|
115
123
|
handleOnLeave();
|
|
116
|
-
}
|
|
117
|
-
|
|
124
|
+
};
|
|
125
|
+
var onFocus = function () {
|
|
118
126
|
pageLeft = false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
document.addEventListener("pagehide", function () {
|
|
127
|
+
};
|
|
128
|
+
var onPageHide = function () {
|
|
122
129
|
isAccessible = document.visibilityState !== "hidden";
|
|
123
130
|
handleOnLeave();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
document.addEventListener("visibilitychange", function () {
|
|
131
|
+
};
|
|
132
|
+
var onVisibilityChange = function () {
|
|
127
133
|
isAccessible = document.visibilityState !== "hidden";
|
|
128
134
|
if (document.visibilityState === "hidden") {
|
|
129
135
|
handleOnLeave();
|
|
@@ -131,7 +137,36 @@ var EventQueue = /** @class */ (function () {
|
|
|
131
137
|
else {
|
|
132
138
|
pageLeft = false;
|
|
133
139
|
}
|
|
134
|
-
}
|
|
140
|
+
};
|
|
141
|
+
// Captured at install time, not read again at removal time. Teardown can
|
|
142
|
+
// run after the host has swapped or removed these globals (a test harness
|
|
143
|
+
// replacing jsdom between specs, for one), and removing a listener from a
|
|
144
|
+
// different object than it was added to silently leaves it attached.
|
|
145
|
+
var globalTarget = globalThis;
|
|
146
|
+
var documentTarget = document;
|
|
147
|
+
// Catches the unloading of the page (e.g., closing the tab or navigating away).
|
|
148
|
+
// Includes user actions like clicking a link, entering a new URL,
|
|
149
|
+
// refreshing the page, or closing the browser tab
|
|
150
|
+
// Note that 'pagehide' is not supported in IE.
|
|
151
|
+
// So, this is a fallback.
|
|
152
|
+
globalTarget.addEventListener("beforeunload", onBeforeUnload);
|
|
153
|
+
globalTarget.addEventListener("blur", onBlur);
|
|
154
|
+
globalTarget.addEventListener("focus", onFocus);
|
|
155
|
+
// Catches the page being hidden, including scenarios like closing the tab.
|
|
156
|
+
documentTarget.addEventListener("pagehide", onPageHide);
|
|
157
|
+
// Catches visibility changes, such as switching tabs or minimizing the browser.
|
|
158
|
+
documentTarget.addEventListener("visibilitychange", onVisibilityChange);
|
|
159
|
+
return function () {
|
|
160
|
+
if (resetTimer) {
|
|
161
|
+
clearTimeout(resetTimer);
|
|
162
|
+
resetTimer = null;
|
|
163
|
+
}
|
|
164
|
+
globalTarget.removeEventListener("beforeunload", onBeforeUnload);
|
|
165
|
+
globalTarget.removeEventListener("blur", onBlur);
|
|
166
|
+
globalTarget.removeEventListener("focus", onFocus);
|
|
167
|
+
documentTarget.removeEventListener("pagehide", onPageHide);
|
|
168
|
+
documentTarget.removeEventListener("visibilitychange", onVisibilityChange);
|
|
169
|
+
};
|
|
135
170
|
};
|
|
136
171
|
options = options || {};
|
|
137
172
|
this.queue = [];
|
|
@@ -151,7 +186,7 @@ var EventQueue = /** @class */ (function () {
|
|
|
151
186
|
this.errorHandler = options.errorHandler;
|
|
152
187
|
this.pendingFlush = null;
|
|
153
188
|
this.timer = null;
|
|
154
|
-
this.onPageLeave(function (isAccessible) { return __awaiter(_this, void 0, void 0, function () {
|
|
189
|
+
this.disposePageLeave = this.onPageLeave(function (isAccessible) { return __awaiter(_this, void 0, void 0, function () {
|
|
155
190
|
return __generator(this, function (_a) {
|
|
156
191
|
switch (_a.label) {
|
|
157
192
|
case 0:
|
|
@@ -189,6 +224,39 @@ var EventQueue = /** @class */ (function () {
|
|
|
189
224
|
this.queueByteSize = 0;
|
|
190
225
|
this.payloadHashes.clear();
|
|
191
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* Terminal shutdown. Unlike clear(), which only empties the buffer and can
|
|
229
|
+
* be followed by more events (consent is re-granted, say), close() makes
|
|
230
|
+
* this queue permanently inert: every later enqueue() is a no-op and the
|
|
231
|
+
* page-leave listeners are removed. flush() needs no guard of its own,
|
|
232
|
+
* because a closed queue starts empty and can never be filled again.
|
|
233
|
+
*
|
|
234
|
+
* This is the guarantee a torn-down SDK instance needs. Emission decisions
|
|
235
|
+
* and the emission itself are separated by an await in many call paths
|
|
236
|
+
* (event creation is async), so guarding the *caller* cannot work: the
|
|
237
|
+
* instance may be destroyed while the continuation is in flight. Enforcing
|
|
238
|
+
* it here means no holder of a stale reference can ever send.
|
|
239
|
+
*
|
|
240
|
+
* What close() deliberately does NOT stop is a flush already in flight.
|
|
241
|
+
* Those events were accepted while the instance was alive, so they are
|
|
242
|
+
* real data; abandoning them would turn every unmount into silent loss.
|
|
243
|
+
*/
|
|
244
|
+
EventQueue.prototype.close = function () {
|
|
245
|
+
this.closed = true;
|
|
246
|
+
this.clear();
|
|
247
|
+
if (this.disposePageLeave) {
|
|
248
|
+
safeCall(this.disposePageLeave);
|
|
249
|
+
this.disposePageLeave = null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
Object.defineProperty(EventQueue.prototype, "isClosed", {
|
|
253
|
+
/** True once close() has run. Exposed for teardown assertions. */
|
|
254
|
+
get: function () {
|
|
255
|
+
return this.closed;
|
|
256
|
+
},
|
|
257
|
+
enumerable: false,
|
|
258
|
+
configurable: true
|
|
259
|
+
});
|
|
192
260
|
EventQueue.prototype.enqueue = function (event, callback) {
|
|
193
261
|
return __awaiter(this, void 0, void 0, function () {
|
|
194
262
|
var message_id, queueItem, hasReachedFlushAt, hasReachedQueueSize;
|
|
@@ -196,6 +264,10 @@ var EventQueue = /** @class */ (function () {
|
|
|
196
264
|
switch (_a.label) {
|
|
197
265
|
case 0:
|
|
198
266
|
callback = callback || noop;
|
|
267
|
+
// A torn-down instance must never buffer, however late the caller
|
|
268
|
+
// arrives. See close().
|
|
269
|
+
if (this.closed)
|
|
270
|
+
return [2 /*return*/];
|
|
199
271
|
// Refuse to buffer anything once consent is withdrawn.
|
|
200
272
|
if (this.canSend && !this.canSend()) {
|
|
201
273
|
this.clear();
|
|
@@ -204,6 +276,12 @@ var EventQueue = /** @class */ (function () {
|
|
|
204
276
|
return [4 /*yield*/, this.generateMessageId(event)];
|
|
205
277
|
case 1:
|
|
206
278
|
message_id = _a.sent();
|
|
279
|
+
// Re-check after the await. A caller that entered before close() is
|
|
280
|
+
// suspended here, and on a queue that has not flushed yet its event
|
|
281
|
+
// would push and flush immediately - the exact shape of the bug close()
|
|
282
|
+
// exists to stop.
|
|
283
|
+
if (this.closed)
|
|
284
|
+
return [2 /*return*/];
|
|
207
285
|
// check if the message already exists
|
|
208
286
|
if (this.isDuplicate(message_id)) {
|
|
209
287
|
logger.warn("Event already enqueued, try again after ".concat(millisecondsToSecond(this.flushIntervalMs), " seconds."));
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ChainID, Options } from "../types";
|
|
2
|
+
/** Wallet event kinds that `autocapture` can switch on or off individually. */
|
|
3
|
+
export type AutocaptureEventType = "connect" | "disconnect" | "signature" | "transaction" | "chain";
|
|
4
|
+
/**
|
|
5
|
+
* What the policy knows about the event being considered.
|
|
6
|
+
*
|
|
7
|
+
* An object rather than a bare chain id on purpose. The `excludeChains` fix
|
|
8
|
+
* had to thread an explicit chain through every gate because the policy had
|
|
9
|
+
* no identity of its own, and each further per-event input would widen those
|
|
10
|
+
* signatures again. New inputs extend this type instead.
|
|
11
|
+
*/
|
|
12
|
+
export interface TrackingContext {
|
|
13
|
+
/**
|
|
14
|
+
* The event's own chain, when it has one. It wins over the SDK's central
|
|
15
|
+
* chain, because the two differ whenever the event did not come from the
|
|
16
|
+
* active provider: a second wallet signing through its own transport, or a
|
|
17
|
+
* wagmi mutation naming an explicit chain.
|
|
18
|
+
*/
|
|
19
|
+
chainId?: ChainID;
|
|
20
|
+
}
|
|
21
|
+
/** What the policy needs from the SDK it advises. */
|
|
22
|
+
export interface TrackingPolicyDeps {
|
|
23
|
+
/**
|
|
24
|
+
* The CURRENT options.
|
|
25
|
+
*
|
|
26
|
+
* A getter, not a captured object: `FormoAnalytics.options` is public and
|
|
27
|
+
* mutable, so a consumer can replace it after init
|
|
28
|
+
* (`formo.options = { ...formo.options, tracking: false }`). The gates used
|
|
29
|
+
* to read `this.options` on every call, so that took effect immediately,
|
|
30
|
+
* and holding the constructor's object here would silently freeze
|
|
31
|
+
* configuration at init time.
|
|
32
|
+
*/
|
|
33
|
+
options(): Options;
|
|
34
|
+
/** Consent state. Separate from options because the visitor can change it. */
|
|
35
|
+
hasOptedOut(): boolean;
|
|
36
|
+
/** The SDK's central chain, used when an event carries none of its own. */
|
|
37
|
+
currentChainId(): ChainID | undefined;
|
|
38
|
+
}
|
|
39
|
+
export interface ITrackingPolicy {
|
|
40
|
+
shouldTrack(context?: TrackingContext): boolean;
|
|
41
|
+
isTrackingSuppressed(): boolean;
|
|
42
|
+
isChainExcluded(context?: TrackingContext): boolean;
|
|
43
|
+
isPageExcluded(): boolean;
|
|
44
|
+
isPersistedIdentityPurgeRequired(): boolean;
|
|
45
|
+
isAutocaptureEnabled(eventType: AutocaptureEventType): boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Every "may we track this?" decision, in one place.
|
|
49
|
+
*
|
|
50
|
+
* Split out of `FormoAnalytics` so the rules can be read and tested without
|
|
51
|
+
* an SDK instance, and so option parsing happens once rather than being
|
|
52
|
+
* re-derived at each gate.
|
|
53
|
+
*/
|
|
54
|
+
export declare class TrackingPolicy implements ITrackingPolicy {
|
|
55
|
+
private readonly deps;
|
|
56
|
+
constructor(deps: TrackingPolicyDeps);
|
|
57
|
+
/**
|
|
58
|
+
* `tracking` as an options object, or null when it is absent, a boolean, or
|
|
59
|
+
* anything else. Every exclusion rule lives on the object form, so a null
|
|
60
|
+
* here means "no exclusions configured", not "excluded".
|
|
61
|
+
*/
|
|
62
|
+
private trackingOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Visitor-level tracking suppression.
|
|
65
|
+
*
|
|
66
|
+
* True when the SDK must not persist any identity/session/chain state or
|
|
67
|
+
* send any events for this visitor: an explicit opt-out, or a
|
|
68
|
+
* jurisdiction/timezone/host/path exclusion. Public entry points that write
|
|
69
|
+
* state before reaching `shouldTrack()` (identify / connect / detect) check
|
|
70
|
+
* this first, so a suppressed visitor leaves no cookies or session state.
|
|
71
|
+
*/
|
|
72
|
+
isTrackingSuppressed(): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the environment is excluded: the visitor's timezone, the current
|
|
75
|
+
* hostname, or the current pathname.
|
|
76
|
+
*
|
|
77
|
+
* Timezone is visitor-level and stable for the session. Host and path are
|
|
78
|
+
* current-page-level and transient, so a SPA navigating back to an allowed
|
|
79
|
+
* path resumes tracking for later actions.
|
|
80
|
+
*/
|
|
81
|
+
private isEnvironmentExcluded;
|
|
82
|
+
/** Exact match against `tracking.excludeHosts`. */
|
|
83
|
+
private isHostExcluded;
|
|
84
|
+
/** Exact match against `tracking.excludePaths`. */
|
|
85
|
+
private isPathExcluded;
|
|
86
|
+
/**
|
|
87
|
+
* Case-insensitive match of the browser-resolved timezone against
|
|
88
|
+
* `tracking.excludeTimezones`. Client-side and best-effort; see the option's
|
|
89
|
+
* own documentation.
|
|
90
|
+
*/
|
|
91
|
+
private isTimezoneExcluded;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the CURRENT PAGE is excluded, as opposed to the visitor.
|
|
94
|
+
*
|
|
95
|
+
* Host and path exclusions are transient: identity written on an allowed
|
|
96
|
+
* page must survive a visit to an excluded route, so callers that persist
|
|
97
|
+
* or restore identity skip the work here rather than purging.
|
|
98
|
+
*/
|
|
99
|
+
isPageExcluded(): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Whether a persisted identity cookie should be actively purged, not merely
|
|
102
|
+
* left unwritten.
|
|
103
|
+
*
|
|
104
|
+
* Host and path exclusions are deliberately absent: they are transient
|
|
105
|
+
* current-page states, so a cookie legitimately written on an allowed page
|
|
106
|
+
* must survive a visit to an excluded route.
|
|
107
|
+
*/
|
|
108
|
+
isPersistedIdentityPurgeRequired(): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Whether the chain in play is in `tracking.excludeChains`.
|
|
111
|
+
*
|
|
112
|
+
* Separate from `shouldTrack()` so `identify()` can ask *before* mutating
|
|
113
|
+
* identity state. `trackEvent()` drops an excluded event silently, but
|
|
114
|
+
* `identify()` marks the wallet as identified first, so without this an
|
|
115
|
+
* identify on an excluded chain is dedup-marked and then discarded, and the
|
|
116
|
+
* wallet never re-emits for the rest of the session even after switching to
|
|
117
|
+
* an allowed chain. On the Privy path that loses a whole cluster at once.
|
|
118
|
+
*/
|
|
119
|
+
isChainExcluded(context?: TrackingContext): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The shared chain rule, so `shouldTrack()` and `isChainExcluded()` cannot
|
|
122
|
+
* drift apart. Callers have already established that `excludeChains` is
|
|
123
|
+
* non-empty.
|
|
124
|
+
*
|
|
125
|
+
* Fails CLOSED on an unknown chain. `resolveChainIdForProvider` reports 0
|
|
126
|
+
* when it has never heard a chain from the signing wallet, and 0 is in no
|
|
127
|
+
* exclusion list, so treating it as "not excluded" would let through exactly
|
|
128
|
+
* the events an operator excluded: the wallet on the excluded chain is often
|
|
129
|
+
* the one we know least about. An explicit exclusion is a directive, so an
|
|
130
|
+
* unresolvable chain is refused.
|
|
131
|
+
*
|
|
132
|
+
* Keyed on 0 and deliberately NOT on `undefined`. 0 is the explicit "we
|
|
133
|
+
* asked and could not tell" marker. `undefined` means no chain state yet,
|
|
134
|
+
* which is a legitimate transient (the Privy path reconciles a Solana wallet
|
|
135
|
+
* through exactly that state), and refusing it would drop real events.
|
|
136
|
+
* `backfillActiveWallet()` never persists 0, so an unresolvable chain cannot
|
|
137
|
+
* leak into the central value and reach the unscoped events (page / track /
|
|
138
|
+
* identify) that fall back to it.
|
|
139
|
+
*/
|
|
140
|
+
private isChainRefused;
|
|
141
|
+
/** Whether an event may be tracked at all, given consent and configuration. */
|
|
142
|
+
shouldTrack(context?: TrackingContext): boolean;
|
|
143
|
+
/** Whether a wallet event kind is enabled for autocapture. Defaults to on. */
|
|
144
|
+
isAutocaptureEnabled(eventType: AutocaptureEventType): boolean;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=TrackingPolicy.d.ts.map
|