@formo/analytics 1.29.1 → 1.30.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.
Files changed (40) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +20 -3
  2. package/dist/cjs/src/FormoAnalytics.js +102 -9
  3. package/dist/cjs/src/event/EventFactory.d.ts +1 -1
  4. package/dist/cjs/src/event/EventFactory.js +3 -3
  5. package/dist/cjs/src/event/EventManager.d.ts +2 -0
  6. package/dist/cjs/src/event/EventManager.js +4 -0
  7. package/dist/cjs/src/event/type.d.ts +1 -0
  8. package/dist/cjs/src/event/utils.js +1 -1
  9. package/dist/cjs/src/queue/EventQueue.d.ts +8 -0
  10. package/dist/cjs/src/queue/EventQueue.js +52 -6
  11. package/dist/cjs/src/queue/type.d.ts +1 -0
  12. package/dist/cjs/src/session/index.js +16 -8
  13. package/dist/cjs/src/storage/cookiePolicy.d.ts +16 -0
  14. package/dist/cjs/src/storage/cookiePolicy.js +19 -0
  15. package/dist/cjs/src/types/base.d.ts +0 -1
  16. package/dist/cjs/src/types/events.d.ts +0 -1
  17. package/dist/cjs/src/version.d.ts +1 -1
  18. package/dist/cjs/src/version.js +1 -1
  19. package/dist/cjs/src/wagmi/WagmiEventHandler.js +38 -15
  20. package/dist/esm/src/FormoAnalytics.d.ts +20 -3
  21. package/dist/esm/src/FormoAnalytics.js +105 -12
  22. package/dist/esm/src/event/EventFactory.d.ts +1 -1
  23. package/dist/esm/src/event/EventFactory.js +3 -3
  24. package/dist/esm/src/event/EventManager.d.ts +2 -0
  25. package/dist/esm/src/event/EventManager.js +4 -0
  26. package/dist/esm/src/event/type.d.ts +1 -0
  27. package/dist/esm/src/event/utils.js +2 -2
  28. package/dist/esm/src/queue/EventQueue.d.ts +8 -0
  29. package/dist/esm/src/queue/EventQueue.js +52 -6
  30. package/dist/esm/src/queue/type.d.ts +1 -0
  31. package/dist/esm/src/session/index.js +16 -8
  32. package/dist/esm/src/storage/cookiePolicy.d.ts +16 -0
  33. package/dist/esm/src/storage/cookiePolicy.js +18 -0
  34. package/dist/esm/src/types/base.d.ts +0 -1
  35. package/dist/esm/src/types/events.d.ts +0 -1
  36. package/dist/esm/src/version.d.ts +1 -1
  37. package/dist/esm/src/version.js +1 -1
  38. package/dist/esm/src/wagmi/WagmiEventHandler.js +38 -15
  39. package/dist/index.umd.min.js +1 -1
  40. package/package.json +7 -18
@@ -173,6 +173,11 @@ var WagmiEventHandler = /** @class */ (function () {
173
173
  chainId: chainId,
174
174
  });
175
175
  if (!(status === "disconnected" && prevStatus === "connected")) return [3 /*break*/, 4];
176
+ // Clear central chain state regardless of autocapture so a later
177
+ // event can't carry a stale excluded/!excluded chainId.
178
+ this.formo.syncWalletState({
179
+ chainId: this.trackingState.lastChainId,
180
+ });
176
181
  if (!this.formo.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 3];
177
182
  return [4 /*yield*/, this.formo.disconnect({
178
183
  chainId: this.trackingState.lastChainId,
@@ -190,6 +195,9 @@ var WagmiEventHandler = /** @class */ (function () {
190
195
  if (!(address && chainId !== undefined)) return [3 /*break*/, 6];
191
196
  this.trackingState.lastAddress = address;
192
197
  this.trackingState.lastChainId = chainId;
198
+ // Sync central state unconditionally so tracking.excludeChains
199
+ // is enforced even when connect autocapture is disabled.
200
+ this.formo.syncWalletState({ chainId: chainId, address: address });
193
201
  if (!this.formo.isAutocaptureEnabled("connect")) return [3 /*break*/, 6];
194
202
  connectorName = this.getConnectorName(state);
195
203
  return [4 /*yield*/, this.formo.connect({ chainId: chainId, address: address }, __assign({}, (connectorName && { providerName: connectorName })))];
@@ -238,6 +246,9 @@ var WagmiEventHandler = /** @class */ (function () {
238
246
  address: address,
239
247
  });
240
248
  this.trackingState.lastChainId = chainId;
249
+ // Sync central state unconditionally so a chain switch to an
250
+ // excluded chain is honored even when chain autocapture is disabled.
251
+ this.formo.syncWalletState({ chainId: chainId, address: address });
241
252
  if (!this.formo.isAutocaptureEnabled("chain")) return [3 /*break*/, 4];
242
253
  _a.label = 1;
243
254
  case 1:
@@ -359,14 +370,21 @@ var WagmiEventHandler = /** @class */ (function () {
359
370
  logger_1.logger.warn("WagmiEventHandler: Transaction receipt query but no hash found");
360
371
  return;
361
372
  }
362
- // Retrieve stored transaction details from BROADCASTED event
363
- // Normalize hash to lowercase for consistent lookup
373
+ // Retrieve stored transaction details from the BROADCASTED event.
374
+ // Normalize hash to lowercase for consistent lookup.
364
375
  var normalizedHash = transactionHash.toLowerCase();
365
376
  var pendingTx = this.pendingTransactions.get(normalizedHash);
366
- // Use the original sender address from BROADCASTED event if available,
367
- // otherwise fall back to current connected address.
368
- // This handles wallet switches between broadcast and confirmation.
369
- var address = (pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.address) || this.trackingState.lastAddress;
377
+ // Only emit receipt-derived events for a hash we actually observed
378
+ // being broadcast through this handler. The QueryClient is supplied
379
+ // by the host app and its cache can be written by app code or other
380
+ // deps, so without this gate a forged waitForTransactionReceipt
381
+ // entry could fabricate a confirmed/reverted transaction for an
382
+ // arbitrary hash. No pendingTx → not our broadcast → ignore.
383
+ if (!pendingTx) {
384
+ logger_1.logger.debug("WagmiEventHandler: Receipt for unobserved tx hash; ignoring", { transactionHash: transactionHash });
385
+ return;
386
+ }
387
+ var address = pendingTx.address;
370
388
  if (!address) {
371
389
  logger_1.logger.warn("WagmiEventHandler: Transaction receipt query but no address available");
372
390
  return;
@@ -374,9 +392,13 @@ var WagmiEventHandler = /** @class */ (function () {
374
392
  try {
375
393
  // Extract receipt data
376
394
  var receipt = state.data;
377
- // Determine transaction status from receipt
378
- // receipt.status is 'success' or 'reverted' in viem
379
- var txStatus = (receipt === null || receipt === void 0 ? void 0 : receipt.status) === "reverted"
395
+ // Only act on an explicit on-chain outcome. A missing/unknown
396
+ // status must NOT be treated as a confirmation.
397
+ if ((receipt === null || receipt === void 0 ? void 0 : receipt.status) !== "success" && (receipt === null || receipt === void 0 ? void 0 : receipt.status) !== "reverted") {
398
+ logger_1.logger.debug("WagmiEventHandler: Receipt without explicit success/reverted status; ignoring", { transactionHash: transactionHash, status: receipt === null || receipt === void 0 ? void 0 : receipt.status });
399
+ return;
400
+ }
401
+ var txStatus = receipt.status === "reverted"
380
402
  ? events_1.TransactionStatus.REVERTED
381
403
  : events_1.TransactionStatus.CONFIRMED;
382
404
  logger_1.logger.info("WagmiEventHandler: Tracking transaction confirmation", {
@@ -455,15 +477,13 @@ var WagmiEventHandler = /** @class */ (function () {
455
477
  return;
456
478
  }
457
479
  try {
458
- // Map Wagmi mutation status to Formo signature status
480
+ // Map Wagmi mutation status to Formo signature status.
459
481
  var status_1;
460
- var signatureHash = void 0;
461
482
  if (state.status === "pending") {
462
483
  status_1 = events_1.SignatureStatus.REQUESTED;
463
484
  }
464
485
  else if (state.status === "success") {
465
486
  status_1 = events_1.SignatureStatus.CONFIRMED;
466
- signatureHash = state.data;
467
487
  }
468
488
  else if (state.status === "error") {
469
489
  status_1 = events_1.SignatureStatus.REJECTED;
@@ -471,13 +491,11 @@ var WagmiEventHandler = /** @class */ (function () {
471
491
  else {
472
492
  return; // Ignore idle state
473
493
  }
474
- // Extract message from variables
475
494
  var message = void 0;
476
495
  if (mutationType === "signMessage") {
477
496
  message = variables.message || "";
478
497
  }
479
498
  else {
480
- // For signTypedData, stringify the typed data
481
499
  message = JSON.stringify(variables.message || variables.types || {});
482
500
  }
483
501
  logger_1.logger.info("WagmiEventHandler: Tracking signature event", {
@@ -486,7 +504,12 @@ var WagmiEventHandler = /** @class */ (function () {
486
504
  address: address,
487
505
  chainId: chainId,
488
506
  });
489
- this.formo.signature(__assign({ status: status_1, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })));
507
+ this.formo.signature({
508
+ status: status_1,
509
+ chainId: chainId,
510
+ address: address,
511
+ message: message,
512
+ });
490
513
  }
491
514
  catch (error) {
492
515
  logger_1.logger.error("WagmiEventHandler: Error handling signature mutation:", error);
@@ -136,18 +136,16 @@ export declare class FormoAnalytics implements IFormoAnalytics {
136
136
  * @param {ChainID} params.chainId
137
137
  * @param {Address} params.address
138
138
  * @param {string} params.message
139
- * @param {string} params.signatureHash - only provided if status is confirmed
140
139
  * @param {IFormoEventProperties} properties
141
140
  * @param {IFormoEventContext} context
142
141
  * @param {(...args: unknown[]) => void} callback
143
142
  * @returns {Promise<void>}
144
143
  */
145
- signature({ status, chainId, address, message, signatureHash, }: {
144
+ signature({ status, chainId, address, message, }: {
146
145
  status: SignatureStatus;
147
146
  chainId?: ChainID;
148
147
  address: Address;
149
148
  message: string;
150
- signatureHash?: string;
151
149
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
152
150
  /**
153
151
  * Emits a transaction event.
@@ -381,6 +379,25 @@ export declare class FormoAnalytics implements IFormoAnalytics {
381
379
  * Clear per-chain state for a given namespace (or chainId) and sync derived state.
382
380
  */
383
381
  private clearChainState;
382
+ /**
383
+ * Sync validated wallet/chain state into the SDK's central state
384
+ * WITHOUT emitting an event.
385
+ *
386
+ * Integrations (e.g. the wagmi handler) must call this on every
387
+ * connect / chain-change / disconnect — even when the corresponding
388
+ * autocapture event is disabled. Otherwise `currentChainId` stays
389
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
390
+ * check (which keys off `currentChainId`, not the event payload) can
391
+ * be bypassed, letting wallet activity on an excluded chain still be
392
+ * collected.
393
+ *
394
+ * - valid `address` present → record per-chain + derived state
395
+ * - `address` absent → clear chain state (disconnect)
396
+ */
397
+ syncWalletState(params: {
398
+ chainId?: ChainID;
399
+ address?: Address;
400
+ }): void;
384
401
  /**
385
402
  * Synchronize currentAddress/currentChainId from the active namespace.
386
403
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -55,9 +55,9 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
55
55
  return to.concat(ar || Array.prototype.slice.call(from));
56
56
  };
57
57
  import { createStore } from "mipd";
58
- import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_USER_ID_KEY, ACTIVE_WALLET_KEY, ACTIVE_WALLET_TTL_MS, CONSENT_OPT_OUT_KEY, } from "./constants";
59
- import { cookie, initStorageManager } from "./storage";
60
- import { getIdentityCookieDomain } from "./storage/cookiePolicy";
58
+ import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_USER_ID_KEY, SESSION_TRAFFIC_SOURCE_KEY, ACTIVE_WALLET_KEY, ACTIVE_WALLET_TTL_MS, CONSENT_OPT_OUT_KEY, } from "./constants";
59
+ import { cookie, session, initStorageManager } from "./storage";
60
+ import { getIdentityCookieDomain, getIdentityCookieSecurity, } from "./storage/cookiePolicy";
61
61
  import { EventManager } from "./event";
62
62
  import { EventQueue } from "./queue";
63
63
  import { logger, Logger } from "./logger";
@@ -82,6 +82,7 @@ var PROVIDER_SWITCH_REASONS = {
82
82
  var FormoAnalytics = /** @class */ (function () {
83
83
  function FormoAnalytics(writeKey, options) {
84
84
  if (options === void 0) { options = {}; }
85
+ var _this = this;
85
86
  var _a, _b, _c;
86
87
  this.writeKey = writeKey;
87
88
  this.options = options;
@@ -157,6 +158,10 @@ var FormoAnalytics = /** @class */ (function () {
157
158
  maxQueueSize: options.maxQueueSize,
158
159
  flushInterval: options.flushInterval,
159
160
  errorHandler: options.errorHandler,
161
+ // Hard consent gate at the queue boundary: nothing buffered is
162
+ // ever sent once the user has opted out, even via a timer or
163
+ // pagehide flush scheduled before opt-out.
164
+ canSend: function () { return !_this.hasOptedOutTracking(); },
160
165
  }), options);
161
166
  // Check consent status on initialization
162
167
  if (this.hasOptedOutTracking()) {
@@ -294,11 +299,25 @@ var FormoAnalytics = /** @class */ (function () {
294
299
  */
295
300
  FormoAnalytics.prototype.reset = function () {
296
301
  this.currentUserId = undefined;
302
+ // Clear in-memory wallet identity too. Without this, a logout/reset
303
+ // (also triggered by optOutTracking) still leaks the previous wallet
304
+ // address on subsequent track()/page() events for the rest of the
305
+ // page lifetime, because they fall back to currentAddress. Keep the
306
+ // EVM provider reference so tracking can resume on the next connect.
307
+ this.currentAddress = undefined;
308
+ this.currentChainId = undefined;
309
+ var evmProvider = this._chainState.evm.provider;
310
+ this._chainState = { evm: { provider: evmProvider }, solana: {} };
311
+ this._activeNamespace = undefined;
297
312
  cookie().remove(LOCAL_ANONYMOUS_ID_KEY);
298
313
  cookie().remove(SESSION_USER_ID_KEY);
299
314
  cookie().remove(SESSION_WALLET_DETECTED_KEY);
300
315
  cookie().remove(SESSION_WALLET_IDENTIFIED_KEY);
301
316
  cookie().remove(ACTIVE_WALLET_KEY);
317
+ // Stored traffic-source attribution (referrer/UTM) is tracking data;
318
+ // clear it too so reset()/optOutTracking() don't leave it to be
319
+ // re-attached to the next session's events.
320
+ session().remove(SESSION_TRAFFIC_SOURCE_KEY);
302
321
  };
303
322
  /**
304
323
  * Clean up resources and event listeners
@@ -307,6 +326,8 @@ var FormoAnalytics = /** @class */ (function () {
307
326
  */
308
327
  FormoAnalytics.prototype.cleanup = function () {
309
328
  logger.debug("FormoAnalytics: Cleaning up resources");
329
+ // Drop buffered events so a torn-down instance can't flush later.
330
+ this.eventManager.clear();
310
331
  // Clean up Wagmi handler if present
311
332
  if (this.wagmiHandler) {
312
333
  this.wagmiHandler.cleanup();
@@ -470,7 +491,6 @@ var FormoAnalytics = /** @class */ (function () {
470
491
  * @param {ChainID} params.chainId
471
492
  * @param {Address} params.address
472
493
  * @param {string} params.message
473
- * @param {string} params.signatureHash - only provided if status is confirmed
474
494
  * @param {IFormoEventProperties} properties
475
495
  * @param {IFormoEventContext} context
476
496
  * @param {(...args: unknown[]) => void} callback
@@ -478,10 +498,15 @@ var FormoAnalytics = /** @class */ (function () {
478
498
  */
479
499
  FormoAnalytics.prototype.signature = function (_a, properties_1, context_1, callback_1) {
480
500
  return __awaiter(this, arguments, void 0, function (_b, properties, context, callback) {
481
- var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message, signatureHash = _b.signatureHash;
501
+ var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message;
482
502
  return __generator(this, function (_c) {
483
503
  switch (_c.label) {
484
- case 0: return [4 /*yield*/, this.trackEvent(EventType.SIGNATURE, __assign({ status: status, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })), properties, context, callback)];
504
+ case 0: return [4 /*yield*/, this.trackEvent(EventType.SIGNATURE, {
505
+ status: status,
506
+ chainId: chainId,
507
+ address: address,
508
+ message: message,
509
+ }, properties, context, callback)];
485
510
  case 1:
486
511
  _c.sent();
487
512
  return [2 /*return*/];
@@ -552,6 +577,13 @@ var FormoAnalytics = /** @class */ (function () {
552
577
  switch (_d.label) {
553
578
  case 0:
554
579
  _d.trys.push([0, 12, , 13]);
580
+ // identify() writes the user-id cookie and marks wallet
581
+ // identification before trackEvent's consent check — gate the
582
+ // whole method so an opted-out user gets no identity persistence.
583
+ if (this.hasOptedOutTracking()) {
584
+ logger.info("identify() skipped: user has opted out of tracking");
585
+ return [2 /*return*/];
586
+ }
555
587
  if (!!params) return [3 /*break*/, 10];
556
588
  // If no params provided, auto-identify
557
589
  logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
@@ -627,7 +659,7 @@ var FormoAnalytics = /** @class */ (function () {
627
659
  if (userId) {
628
660
  this.currentUserId = userId;
629
661
  domain = getIdentityCookieDomain(this.crossSubdomainCookies);
630
- cookie().set(SESSION_USER_ID_KEY, userId, __assign({ path: "/" }, (domain ? { domain: domain } : {})));
662
+ cookie().set(SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
631
663
  }
632
664
  isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
633
665
  logger.debug("Identify: Checking deduplication", {
@@ -676,6 +708,12 @@ var FormoAnalytics = /** @class */ (function () {
676
708
  return __generator(this, function (_c) {
677
709
  switch (_c.label) {
678
710
  case 0:
711
+ // detect() marks wallet detection (a cookie write) before
712
+ // trackEvent's consent check — gate it on opt-out.
713
+ if (this.hasOptedOutTracking()) {
714
+ logger.info("detect() skipped: user has opted out of tracking");
715
+ return [2 /*return*/];
716
+ }
679
717
  if (this.session.isWalletDetected(rdns))
680
718
  return [2 /*return*/, logger.warn("Detect: Wallet ".concat(providerName, " already detected in this session"))];
681
719
  this.session.markWalletDetected(rdns);
@@ -722,6 +760,9 @@ var FormoAnalytics = /** @class */ (function () {
722
760
  // Set opt-out flag in persistent storage using direct cookie access
723
761
  // This must be done before switching storage to ensure persistence
724
762
  setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
763
+ // Drop anything already buffered so a pending timer/pagehide flush
764
+ // cannot ship events after consent withdrawal.
765
+ this.eventManager.clear();
725
766
  this.reset();
726
767
  logger.info("Successfully opted out of tracking");
727
768
  };
@@ -1941,7 +1982,9 @@ var FormoAnalytics = /** @class */ (function () {
1941
1982
  });
1942
1983
  });
1943
1984
  };
1944
- FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params, response, chainId) {
1985
+ FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
1986
+ // Intentionally not read. Kept for positional call-site arity.
1987
+ _response, chainId) {
1945
1988
  var _a;
1946
1989
  var rawAddress = method === "personal_sign"
1947
1990
  ? params[1]
@@ -1958,9 +2001,10 @@ var FormoAnalytics = /** @class */ (function () {
1958
2001
  };
1959
2002
  if (method === "personal_sign") {
1960
2003
  var message = Buffer.from(params[0].slice(2), "hex").toString("utf8");
1961
- return __assign(__assign(__assign({}, basePayload), { message: message }), (response ? { signatureHash: response } : {}));
2004
+ return __assign(__assign({}, basePayload), { message: message });
1962
2005
  }
1963
- return __assign(__assign(__assign({}, basePayload), { message: params[1] }), (response ? { signatureHash: response } : {}));
2006
+ // eth_signTypedData*: params[1] is the full EIP-712 struct.
2007
+ return __assign(__assign({}, basePayload), { message: params[1] });
1964
2008
  };
1965
2009
  FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
1966
2010
  return __awaiter(this, void 0, void 0, function () {
@@ -2076,7 +2120,8 @@ var FormoAnalytics = /** @class */ (function () {
2076
2120
  }
2077
2121
  this._providerListenersMap.delete(provider);
2078
2122
  };
2079
- // Explicitly untrack a provider: remove listeners, clear wrapper flag and tracking
2123
+ // Explicitly untrack a provider: remove listeners, clear wrapper flag
2124
+ // and tracking
2080
2125
  FormoAnalytics.prototype.untrackProvider = function (provider) {
2081
2126
  try {
2082
2127
  this.removeProviderListeners(provider);
@@ -2194,6 +2239,42 @@ var FormoAnalytics = /** @class */ (function () {
2194
2239
  }
2195
2240
  this.syncDerivedState();
2196
2241
  };
2242
+ /**
2243
+ * Sync validated wallet/chain state into the SDK's central state
2244
+ * WITHOUT emitting an event.
2245
+ *
2246
+ * Integrations (e.g. the wagmi handler) must call this on every
2247
+ * connect / chain-change / disconnect — even when the corresponding
2248
+ * autocapture event is disabled. Otherwise `currentChainId` stays
2249
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2250
+ * check (which keys off `currentChainId`, not the event payload) can
2251
+ * be bypassed, letting wallet activity on an excluded chain still be
2252
+ * collected.
2253
+ *
2254
+ * - valid `address` present → record per-chain + derived state
2255
+ * - `address` absent → clear chain state (disconnect)
2256
+ */
2257
+ FormoAnalytics.prototype.syncWalletState = function (params) {
2258
+ var chainId = params.chainId, address = params.address;
2259
+ if (!address) {
2260
+ if (chainId !== undefined && chainId !== null) {
2261
+ this.clearChainState(chainId);
2262
+ }
2263
+ else {
2264
+ this.clearChainState("evm");
2265
+ this.clearChainState("solana");
2266
+ }
2267
+ return;
2268
+ }
2269
+ if (chainId === null || chainId === undefined)
2270
+ return;
2271
+ var validAddress = validateAddress(address, chainId);
2272
+ if (!validAddress) {
2273
+ logger.warn("syncWalletState: invalid address (\"".concat(address, "\") for chain ").concat(chainId));
2274
+ return;
2275
+ }
2276
+ this.setChainState(chainId, { address: validAddress });
2277
+ };
2197
2278
  /**
2198
2279
  * Synchronize currentAddress/currentChainId from the active namespace.
2199
2280
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -2232,10 +2313,16 @@ var FormoAnalytics = /** @class */ (function () {
2232
2313
  */
2233
2314
  FormoAnalytics.prototype.persistActiveWallet = function () {
2234
2315
  try {
2316
+ // Never write an identity cookie for an opted-out user; ensure any
2317
+ // prior snapshot is removed instead.
2318
+ if (this.hasOptedOutTracking()) {
2319
+ cookie().remove(ACTIVE_WALLET_KEY);
2320
+ return;
2321
+ }
2235
2322
  if (this.currentAddress) {
2236
2323
  var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
2237
2324
  var domain = getIdentityCookieDomain(this.crossSubdomainCookies);
2238
- cookie().set(ACTIVE_WALLET_KEY, value, __assign({ path: "/", expires: new Date(Date.now() + ACTIVE_WALLET_TTL_MS).toUTCString() }, (domain ? { domain: domain } : {})));
2325
+ cookie().set(ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + ACTIVE_WALLET_TTL_MS).toUTCString() }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
2239
2326
  }
2240
2327
  else {
2241
2328
  cookie().remove(ACTIVE_WALLET_KEY);
@@ -2251,6 +2338,12 @@ var FormoAnalytics = /** @class */ (function () {
2251
2338
  */
2252
2339
  FormoAnalytics.prototype.loadActiveWallet = function () {
2253
2340
  try {
2341
+ // Never restore wallet identity into memory for an opted-out user
2342
+ // (mirrors persistActiveWallet's guard); drop any stale snapshot.
2343
+ if (this.hasOptedOutTracking()) {
2344
+ cookie().remove(ACTIVE_WALLET_KEY);
2345
+ return;
2346
+ }
2254
2347
  var raw = cookie().get(ACTIVE_WALLET_KEY);
2255
2348
  if (!raw)
2256
2349
  return;
@@ -35,7 +35,7 @@ declare class EventFactory implements IEventFactory {
35
35
  generateConnectEvent(chainId: ChainID, address: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
36
36
  generateDisconnectEvent(chainId?: ChainID, address?: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
37
37
  generateChainChangedEvent(chainId: ChainID, address: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
38
- generateSignatureEvent(status: SignatureStatus, chainId: ChainID, address: Address, message: string, signatureHash?: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
38
+ generateSignatureEvent(status: SignatureStatus, chainId: ChainID, address: Address, message: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
39
39
  generateTransactionEvent(status: TransactionStatus, chainId: ChainID, address: Address, data?: string, to?: string, value?: string, transactionHash?: string, function_name?: string, function_args?: Record<string, unknown>, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
40
40
  generateTrackEvent(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
41
41
  create(event: APIEvent, address?: Address, userId?: string): Promise<IFormoEvent>;
@@ -413,12 +413,12 @@ var EventFactory = /** @class */ (function () {
413
413
  });
414
414
  });
415
415
  };
416
- EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, signatureHash, properties, context) {
416
+ EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, properties, context) {
417
417
  return __awaiter(this, void 0, void 0, function () {
418
418
  var signatureEvent;
419
419
  return __generator(this, function (_a) {
420
420
  signatureEvent = {
421
- properties: __assign(__assign({ status: status, chainId: chainId, message: message }, (signatureHash && { signatureHash: signatureHash })), properties),
421
+ properties: __assign({ status: status, chainId: chainId, message: message }, properties),
422
422
  address: address,
423
423
  type: "signature",
424
424
  };
@@ -506,7 +506,7 @@ var EventFactory = /** @class */ (function () {
506
506
  case 12:
507
507
  formoEvent = _b.sent();
508
508
  return [3 /*break*/, 19];
509
- case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.signatureHash, event.properties, event.context)];
509
+ case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.properties, event.context)];
510
510
  case 14:
511
511
  formoEvent = _b.sent();
512
512
  return [3 /*break*/, 19];
@@ -18,6 +18,8 @@ declare class EventManager implements IEventManager {
18
18
  * @param event Incoming event data
19
19
  */
20
20
  addEvent(event: APIEvent, address?: Address, userId?: string): Promise<void>;
21
+ /** Drop any buffered events (consent withdrawal / teardown). */
22
+ clear(): void;
21
23
  }
22
24
  export { EventManager };
23
25
  //# sourceMappingURL=EventManager.d.ts.map
@@ -93,6 +93,10 @@ var EventManager = /** @class */ (function () {
93
93
  });
94
94
  });
95
95
  };
96
+ /** Drop any buffered events (consent withdrawal / teardown). */
97
+ EventManager.prototype.clear = function () {
98
+ this.eventQueue.clear();
99
+ };
96
100
  return EventManager;
97
101
  }());
98
102
  export { EventManager };
@@ -1,6 +1,7 @@
1
1
  import { Address, APIEvent, IFormoEvent } from "../types";
2
2
  export interface IEventManager {
3
3
  addEvent(event: APIEvent, address?: Address, userId?: string): Promise<void>;
4
+ clear(): void;
4
5
  }
5
6
  export interface IEventFactory {
6
7
  create(event: APIEvent, address?: Address, userId?: string): Promise<IFormoEvent>;
@@ -11,7 +11,7 @@ var __assign = (this && this.__assign) || function () {
11
11
  };
12
12
  import { generateNativeUUID } from "../utils";
13
13
  import { cookie } from "../storage";
14
- import { getIdentityCookieDomain } from "../storage/cookiePolicy";
14
+ import { getIdentityCookieDomain, getIdentityCookieSecurity, } from "../storage/cookiePolicy";
15
15
  var generateAnonymousId = function (key, crossSubdomainCookies) {
16
16
  var storedAnonymousId = cookie().get(key);
17
17
  var anonymousId = (storedAnonymousId && typeof storedAnonymousId === "string"
@@ -23,7 +23,7 @@ var generateAnonymousId = function (key, crossSubdomainCookies) {
23
23
  // domain. Note: host-only cookies on other hosts (e.g. a cookie set on
24
24
  // example.com is not visible from app.example.com) cannot be migrated
25
25
  // until the user revisits that host.
26
- cookie().set(key, anonymousId, __assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (domain ? { domain: domain } : {})));
26
+ cookie().set(key, anonymousId, __assign(__assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
27
27
  return anonymousId;
28
28
  };
29
29
  export { generateAnonymousId };
@@ -11,6 +11,7 @@ type Options = {
11
11
  retryCount?: number;
12
12
  errorHandler?: any;
13
13
  maxQueueSize?: number;
14
+ canSend?: () => boolean;
14
15
  };
15
16
  export declare class EventQueue implements IEventQueue {
16
17
  private writeKey;
@@ -21,12 +22,19 @@ export declare class EventQueue implements IEventQueue {
21
22
  private flushIntervalMs;
22
23
  private flushed;
23
24
  private maxQueueSize;
25
+ private queueByteSize;
24
26
  private errorHandler;
25
27
  private retryCount;
26
28
  private pendingFlush;
27
29
  private payloadHashes;
30
+ private canSend?;
28
31
  constructor(writeKey: string, options: Options);
29
32
  private generateMessageId;
33
+ /**
34
+ * Drop all queued data and cancel the flush timer. Called on consent
35
+ * withdrawal / SDK teardown so nothing buffered can be sent later.
36
+ */
37
+ clear(): void;
30
38
  enqueue(event: IFormoEvent, callback?: (...args: any) => void): Promise<void>;
31
39
  flush(callback?: (...args: any) => void, drainAll?: boolean): Promise<void | IFormoEventFlushPayload[]>;
32
40
  /**
@@ -81,6 +81,7 @@ var EventQueue = /** @class */ (function () {
81
81
  function EventQueue(writeKey, options) {
82
82
  var _this = this;
83
83
  this.queue = [];
84
+ this.queueByteSize = 0; // running total of queued items' byteSize
84
85
  this.payloadHashes = new Set();
85
86
  this.onPageLeave = function (callback) {
86
87
  // To ensure the callback is only called once even if more than one events
@@ -136,6 +137,7 @@ var EventQueue = /** @class */ (function () {
136
137
  this.queue = [];
137
138
  this.writeKey = writeKey;
138
139
  this.apiHost = options.apiHost;
140
+ this.canSend = options.canSend;
139
141
  this.retryCount = clampNumber(options.retryCount || DEFAULT_RETRY, MAX_RETRY, MIN_RETRY);
140
142
  this.flushAt = clampNumber(options.flushAt || DEFAULT_FLUSH_AT, MAX_FLUSH_AT, MIN_FLUSH_AT);
141
143
  this.maxQueueSize = clampNumber(options.maxQueueSize || DEFAULT_QUEUE_SIZE, MAX_QUEUE_SIZE, MIN_QUEUE_SIZE);
@@ -169,13 +171,31 @@ var EventQueue = /** @class */ (function () {
169
171
  });
170
172
  });
171
173
  };
174
+ /**
175
+ * Drop all queued data and cancel the flush timer. Called on consent
176
+ * withdrawal / SDK teardown so nothing buffered can be sent later.
177
+ */
178
+ EventQueue.prototype.clear = function () {
179
+ if (this.timer) {
180
+ clearTimeout(this.timer);
181
+ this.timer = null;
182
+ }
183
+ this.queue = [];
184
+ this.queueByteSize = 0;
185
+ this.payloadHashes.clear();
186
+ };
172
187
  EventQueue.prototype.enqueue = function (event, callback) {
173
188
  return __awaiter(this, void 0, void 0, function () {
174
- var message_id, hasReachedFlushAt, hasReachedQueueSize;
189
+ var message_id, queueItem, hasReachedFlushAt, hasReachedQueueSize;
175
190
  return __generator(this, function (_a) {
176
191
  switch (_a.label) {
177
192
  case 0:
178
193
  callback = callback || noop;
194
+ // Refuse to buffer anything once consent is withdrawn.
195
+ if (this.canSend && !this.canSend()) {
196
+ this.clear();
197
+ return [2 /*return*/];
198
+ }
179
199
  return [4 /*yield*/, this.generateMessageId(event)];
180
200
  case 1:
181
201
  message_id = _a.sent();
@@ -184,10 +204,18 @@ var EventQueue = /** @class */ (function () {
184
204
  logger.warn("Event already enqueued, try again after ".concat(millisecondsToSecond(this.flushIntervalMs), " seconds."));
185
205
  return [2 /*return*/];
186
206
  }
187
- this.queue.push({
207
+ queueItem = {
188
208
  message: __assign(__assign({}, event), { message_id: message_id }),
189
209
  callback: callback,
190
- });
210
+ byteSize: 0,
211
+ };
212
+ // Measure once here (message only — JSON.stringify drops the
213
+ // callback function anyway), then track the total incrementally.
214
+ queueItem.byteSize = JSON.stringify({
215
+ message: queueItem.message,
216
+ }).length;
217
+ this.queue.push(queueItem);
218
+ this.queueByteSize += queueItem.byteSize;
191
219
  logger.log("Event enqueued: ".concat(getActionDescriptor(event.type, event.properties)));
192
220
  if (!this.flushed) {
193
221
  this.flushed = true;
@@ -195,8 +223,7 @@ var EventQueue = /** @class */ (function () {
195
223
  return [2 /*return*/];
196
224
  }
197
225
  hasReachedFlushAt = this.queue.length >= this.flushAt;
198
- hasReachedQueueSize = this.queue.reduce(function (acc, item) { return acc + JSON.stringify(item).length; }, 0) >=
199
- this.maxQueueSize;
226
+ hasReachedQueueSize = this.queueByteSize >= this.maxQueueSize;
200
227
  if (hasReachedFlushAt || hasReachedQueueSize) {
201
228
  this.flush();
202
229
  return [2 /*return*/];
@@ -222,6 +249,13 @@ var EventQueue = /** @class */ (function () {
222
249
  clearTimeout(this.timer);
223
250
  this.timer = null;
224
251
  }
252
+ // Final consent gate: a timer/pagehide flush may have been scheduled
253
+ // before opt-out. Drop everything rather than send post-withdrawal.
254
+ if (this.canSend && !this.canSend()) {
255
+ this.clear();
256
+ callback();
257
+ return [2 /*return*/, Promise.resolve()];
258
+ }
225
259
  if (!this.queue.length) {
226
260
  callback();
227
261
  return [2 /*return*/, Promise.resolve()];
@@ -235,11 +269,17 @@ var EventQueue = /** @class */ (function () {
235
269
  case 2:
236
270
  items = this.queue.splice(0, drainAll ? this.queue.length : this.flushAt);
237
271
  // Only remove hashes for flushed items so duplicate detection remains
238
- // active for events still in the queue.
272
+ // active for events still in the queue. Also decrement the running
273
+ // byte total by exactly what left the queue.
239
274
  for (_i = 0, items_1 = items; _i < items_1.length; _i++) {
240
275
  item = items_1[_i];
241
276
  this.payloadHashes.delete(item.message.message_id);
277
+ this.queueByteSize -= item.byteSize;
242
278
  }
279
+ // Re-anchor to the exact invariant when the queue empties, so any
280
+ // accumulated drift can never wedge the size gate.
281
+ if (this.queue.length === 0)
282
+ this.queueByteSize = 0;
243
283
  sentAt = new Date().toISOString();
244
284
  data = items.map(function (item) { return (__assign(__assign({}, item.message), { sent_at: sentAt })); });
245
285
  batches = this.splitIntoBatches(items, data);
@@ -342,6 +382,12 @@ var EventQueue = /** @class */ (function () {
342
382
  case 1:
343
383
  if (!(_i < batches_1.length)) return [3 /*break*/, 6];
344
384
  batch = batches_1[_i];
385
+ // Consent can be withdrawn while a flush is already in flight:
386
+ // batches were spliced before opt-out, and split batches / retry
387
+ // backoff span seconds. Re-check before every send and abandon
388
+ // the remaining batches if consent was revoked mid-flush.
389
+ if (this.canSend && !this.canSend())
390
+ return [3 /*break*/, 6];
345
391
  _a.label = 2;
346
392
  case 2:
347
393
  _a.trys.push([2, 4, , 5]);