@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
@@ -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.
@@ -85,6 +85,7 @@ var PROVIDER_SWITCH_REASONS = {
85
85
  var FormoAnalytics = /** @class */ (function () {
86
86
  function FormoAnalytics(writeKey, options) {
87
87
  if (options === void 0) { options = {}; }
88
+ var _this = this;
88
89
  var _a, _b, _c;
89
90
  this.writeKey = writeKey;
90
91
  this.options = options;
@@ -160,6 +161,10 @@ var FormoAnalytics = /** @class */ (function () {
160
161
  maxQueueSize: options.maxQueueSize,
161
162
  flushInterval: options.flushInterval,
162
163
  errorHandler: options.errorHandler,
164
+ // Hard consent gate at the queue boundary: nothing buffered is
165
+ // ever sent once the user has opted out, even via a timer or
166
+ // pagehide flush scheduled before opt-out.
167
+ canSend: function () { return !_this.hasOptedOutTracking(); },
163
168
  }), options);
164
169
  // Check consent status on initialization
165
170
  if (this.hasOptedOutTracking()) {
@@ -297,11 +302,25 @@ var FormoAnalytics = /** @class */ (function () {
297
302
  */
298
303
  FormoAnalytics.prototype.reset = function () {
299
304
  this.currentUserId = undefined;
305
+ // Clear in-memory wallet identity too. Without this, a logout/reset
306
+ // (also triggered by optOutTracking) still leaks the previous wallet
307
+ // address on subsequent track()/page() events for the rest of the
308
+ // page lifetime, because they fall back to currentAddress. Keep the
309
+ // EVM provider reference so tracking can resume on the next connect.
310
+ this.currentAddress = undefined;
311
+ this.currentChainId = undefined;
312
+ var evmProvider = this._chainState.evm.provider;
313
+ this._chainState = { evm: { provider: evmProvider }, solana: {} };
314
+ this._activeNamespace = undefined;
300
315
  (0, storage_1.cookie)().remove(constants_1.LOCAL_ANONYMOUS_ID_KEY);
301
316
  (0, storage_1.cookie)().remove(constants_1.SESSION_USER_ID_KEY);
302
317
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_DETECTED_KEY);
303
318
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_IDENTIFIED_KEY);
304
319
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
320
+ // Stored traffic-source attribution (referrer/UTM) is tracking data;
321
+ // clear it too so reset()/optOutTracking() don't leave it to be
322
+ // re-attached to the next session's events.
323
+ (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
305
324
  };
306
325
  /**
307
326
  * Clean up resources and event listeners
@@ -310,6 +329,8 @@ var FormoAnalytics = /** @class */ (function () {
310
329
  */
311
330
  FormoAnalytics.prototype.cleanup = function () {
312
331
  logger_1.logger.debug("FormoAnalytics: Cleaning up resources");
332
+ // Drop buffered events so a torn-down instance can't flush later.
333
+ this.eventManager.clear();
313
334
  // Clean up Wagmi handler if present
314
335
  if (this.wagmiHandler) {
315
336
  this.wagmiHandler.cleanup();
@@ -473,7 +494,6 @@ var FormoAnalytics = /** @class */ (function () {
473
494
  * @param {ChainID} params.chainId
474
495
  * @param {Address} params.address
475
496
  * @param {string} params.message
476
- * @param {string} params.signatureHash - only provided if status is confirmed
477
497
  * @param {IFormoEventProperties} properties
478
498
  * @param {IFormoEventContext} context
479
499
  * @param {(...args: unknown[]) => void} callback
@@ -481,10 +501,15 @@ var FormoAnalytics = /** @class */ (function () {
481
501
  */
482
502
  FormoAnalytics.prototype.signature = function (_a, properties_1, context_1, callback_1) {
483
503
  return __awaiter(this, arguments, void 0, function (_b, properties, context, callback) {
484
- var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message, signatureHash = _b.signatureHash;
504
+ var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message;
485
505
  return __generator(this, function (_c) {
486
506
  switch (_c.label) {
487
- case 0: return [4 /*yield*/, this.trackEvent(constants_1.EventType.SIGNATURE, __assign({ status: status, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })), properties, context, callback)];
507
+ case 0: return [4 /*yield*/, this.trackEvent(constants_1.EventType.SIGNATURE, {
508
+ status: status,
509
+ chainId: chainId,
510
+ address: address,
511
+ message: message,
512
+ }, properties, context, callback)];
488
513
  case 1:
489
514
  _c.sent();
490
515
  return [2 /*return*/];
@@ -555,6 +580,13 @@ var FormoAnalytics = /** @class */ (function () {
555
580
  switch (_d.label) {
556
581
  case 0:
557
582
  _d.trys.push([0, 12, , 13]);
583
+ // identify() writes the user-id cookie and marks wallet
584
+ // identification before trackEvent's consent check — gate the
585
+ // whole method so an opted-out user gets no identity persistence.
586
+ if (this.hasOptedOutTracking()) {
587
+ logger_1.logger.info("identify() skipped: user has opted out of tracking");
588
+ return [2 /*return*/];
589
+ }
558
590
  if (!!params) return [3 /*break*/, 10];
559
591
  // If no params provided, auto-identify
560
592
  logger_1.logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
@@ -630,7 +662,7 @@ var FormoAnalytics = /** @class */ (function () {
630
662
  if (userId) {
631
663
  this.currentUserId = userId;
632
664
  domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
633
- (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign({ path: "/" }, (domain ? { domain: domain } : {})));
665
+ (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
634
666
  }
635
667
  isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
636
668
  logger_1.logger.debug("Identify: Checking deduplication", {
@@ -679,6 +711,12 @@ var FormoAnalytics = /** @class */ (function () {
679
711
  return __generator(this, function (_c) {
680
712
  switch (_c.label) {
681
713
  case 0:
714
+ // detect() marks wallet detection (a cookie write) before
715
+ // trackEvent's consent check — gate it on opt-out.
716
+ if (this.hasOptedOutTracking()) {
717
+ logger_1.logger.info("detect() skipped: user has opted out of tracking");
718
+ return [2 /*return*/];
719
+ }
682
720
  if (this.session.isWalletDetected(rdns))
683
721
  return [2 /*return*/, logger_1.logger.warn("Detect: Wallet ".concat(providerName, " already detected in this session"))];
684
722
  this.session.markWalletDetected(rdns);
@@ -725,6 +763,9 @@ var FormoAnalytics = /** @class */ (function () {
725
763
  // Set opt-out flag in persistent storage using direct cookie access
726
764
  // This must be done before switching storage to ensure persistence
727
765
  (0, consent_1.setConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY, "true");
766
+ // Drop anything already buffered so a pending timer/pagehide flush
767
+ // cannot ship events after consent withdrawal.
768
+ this.eventManager.clear();
728
769
  this.reset();
729
770
  logger_1.logger.info("Successfully opted out of tracking");
730
771
  };
@@ -1944,7 +1985,9 @@ var FormoAnalytics = /** @class */ (function () {
1944
1985
  });
1945
1986
  });
1946
1987
  };
1947
- FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params, response, chainId) {
1988
+ FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
1989
+ // Intentionally not read. Kept for positional call-site arity.
1990
+ _response, chainId) {
1948
1991
  var _a;
1949
1992
  var rawAddress = method === "personal_sign"
1950
1993
  ? params[1]
@@ -1961,9 +2004,10 @@ var FormoAnalytics = /** @class */ (function () {
1961
2004
  };
1962
2005
  if (method === "personal_sign") {
1963
2006
  var message = Buffer.from(params[0].slice(2), "hex").toString("utf8");
1964
- return __assign(__assign(__assign({}, basePayload), { message: message }), (response ? { signatureHash: response } : {}));
2007
+ return __assign(__assign({}, basePayload), { message: message });
1965
2008
  }
1966
- return __assign(__assign(__assign({}, basePayload), { message: params[1] }), (response ? { signatureHash: response } : {}));
2009
+ // eth_signTypedData*: params[1] is the full EIP-712 struct.
2010
+ return __assign(__assign({}, basePayload), { message: params[1] });
1967
2011
  };
1968
2012
  FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
1969
2013
  return __awaiter(this, void 0, void 0, function () {
@@ -2079,7 +2123,8 @@ var FormoAnalytics = /** @class */ (function () {
2079
2123
  }
2080
2124
  this._providerListenersMap.delete(provider);
2081
2125
  };
2082
- // Explicitly untrack a provider: remove listeners, clear wrapper flag and tracking
2126
+ // Explicitly untrack a provider: remove listeners, clear wrapper flag
2127
+ // and tracking
2083
2128
  FormoAnalytics.prototype.untrackProvider = function (provider) {
2084
2129
  try {
2085
2130
  this.removeProviderListeners(provider);
@@ -2197,6 +2242,42 @@ var FormoAnalytics = /** @class */ (function () {
2197
2242
  }
2198
2243
  this.syncDerivedState();
2199
2244
  };
2245
+ /**
2246
+ * Sync validated wallet/chain state into the SDK's central state
2247
+ * WITHOUT emitting an event.
2248
+ *
2249
+ * Integrations (e.g. the wagmi handler) must call this on every
2250
+ * connect / chain-change / disconnect — even when the corresponding
2251
+ * autocapture event is disabled. Otherwise `currentChainId` stays
2252
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2253
+ * check (which keys off `currentChainId`, not the event payload) can
2254
+ * be bypassed, letting wallet activity on an excluded chain still be
2255
+ * collected.
2256
+ *
2257
+ * - valid `address` present → record per-chain + derived state
2258
+ * - `address` absent → clear chain state (disconnect)
2259
+ */
2260
+ FormoAnalytics.prototype.syncWalletState = function (params) {
2261
+ var chainId = params.chainId, address = params.address;
2262
+ if (!address) {
2263
+ if (chainId !== undefined && chainId !== null) {
2264
+ this.clearChainState(chainId);
2265
+ }
2266
+ else {
2267
+ this.clearChainState("evm");
2268
+ this.clearChainState("solana");
2269
+ }
2270
+ return;
2271
+ }
2272
+ if (chainId === null || chainId === undefined)
2273
+ return;
2274
+ var validAddress = (0, address_1.validateAddress)(address, chainId);
2275
+ if (!validAddress) {
2276
+ logger_1.logger.warn("syncWalletState: invalid address (\"".concat(address, "\") for chain ").concat(chainId));
2277
+ return;
2278
+ }
2279
+ this.setChainState(chainId, { address: validAddress });
2280
+ };
2200
2281
  /**
2201
2282
  * Synchronize currentAddress/currentChainId from the active namespace.
2202
2283
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -2235,10 +2316,16 @@ var FormoAnalytics = /** @class */ (function () {
2235
2316
  */
2236
2317
  FormoAnalytics.prototype.persistActiveWallet = function () {
2237
2318
  try {
2319
+ // Never write an identity cookie for an opted-out user; ensure any
2320
+ // prior snapshot is removed instead.
2321
+ if (this.hasOptedOutTracking()) {
2322
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2323
+ return;
2324
+ }
2238
2325
  if (this.currentAddress) {
2239
2326
  var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
2240
2327
  var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
2241
- (0, storage_1.cookie)().set(constants_1.ACTIVE_WALLET_KEY, value, __assign({ path: "/", expires: new Date(Date.now() + constants_1.ACTIVE_WALLET_TTL_MS).toUTCString() }, (domain ? { domain: domain } : {})));
2328
+ (0, storage_1.cookie)().set(constants_1.ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + constants_1.ACTIVE_WALLET_TTL_MS).toUTCString() }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
2242
2329
  }
2243
2330
  else {
2244
2331
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
@@ -2254,6 +2341,12 @@ var FormoAnalytics = /** @class */ (function () {
2254
2341
  */
2255
2342
  FormoAnalytics.prototype.loadActiveWallet = function () {
2256
2343
  try {
2344
+ // Never restore wallet identity into memory for an opted-out user
2345
+ // (mirrors persistActiveWallet's guard); drop any stale snapshot.
2346
+ if (this.hasOptedOutTracking()) {
2347
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2348
+ return;
2349
+ }
2257
2350
  var raw = (0, storage_1.cookie)().get(constants_1.ACTIVE_WALLET_KEY);
2258
2351
  if (!raw)
2259
2352
  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>;
@@ -419,12 +419,12 @@ var EventFactory = /** @class */ (function () {
419
419
  });
420
420
  });
421
421
  };
422
- EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, signatureHash, properties, context) {
422
+ EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, properties, context) {
423
423
  return __awaiter(this, void 0, void 0, function () {
424
424
  var signatureEvent;
425
425
  return __generator(this, function (_a) {
426
426
  signatureEvent = {
427
- properties: __assign(__assign({ status: status, chainId: chainId, message: message }, (signatureHash && { signatureHash: signatureHash })), properties),
427
+ properties: __assign({ status: status, chainId: chainId, message: message }, properties),
428
428
  address: address,
429
429
  type: "signature",
430
430
  };
@@ -512,7 +512,7 @@ var EventFactory = /** @class */ (function () {
512
512
  case 12:
513
513
  formoEvent = _b.sent();
514
514
  return [3 /*break*/, 19];
515
- case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.signatureHash, event.properties, event.context)];
515
+ case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.properties, event.context)];
516
516
  case 14:
517
517
  formoEvent = _b.sent();
518
518
  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
@@ -96,6 +96,10 @@ var EventManager = /** @class */ (function () {
96
96
  });
97
97
  });
98
98
  };
99
+ /** Drop any buffered events (consent withdrawal / teardown). */
100
+ EventManager.prototype.clear = function () {
101
+ this.eventQueue.clear();
102
+ };
99
103
  return EventManager;
100
104
  }());
101
105
  exports.EventManager = 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>;
@@ -26,7 +26,7 @@ var generateAnonymousId = function (key, crossSubdomainCookies) {
26
26
  // domain. Note: host-only cookies on other hosts (e.g. a cookie set on
27
27
  // example.com is not visible from app.example.com) cannot be migrated
28
28
  // until the user revisits that host.
29
- (0, storage_1.cookie)().set(key, anonymousId, __assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (domain ? { domain: domain } : {})));
29
+ (0, storage_1.cookie)().set(key, anonymousId, __assign(__assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
30
30
  return anonymousId;
31
31
  };
32
32
  exports.generateAnonymousId = 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
  /**
@@ -87,6 +87,7 @@ var EventQueue = /** @class */ (function () {
87
87
  function EventQueue(writeKey, options) {
88
88
  var _this = this;
89
89
  this.queue = [];
90
+ this.queueByteSize = 0; // running total of queued items' byteSize
90
91
  this.payloadHashes = new Set();
91
92
  this.onPageLeave = function (callback) {
92
93
  // To ensure the callback is only called once even if more than one events
@@ -142,6 +143,7 @@ var EventQueue = /** @class */ (function () {
142
143
  this.queue = [];
143
144
  this.writeKey = writeKey;
144
145
  this.apiHost = options.apiHost;
146
+ this.canSend = options.canSend;
145
147
  this.retryCount = (0, utils_1.clampNumber)(options.retryCount || DEFAULT_RETRY, MAX_RETRY, MIN_RETRY);
146
148
  this.flushAt = (0, utils_1.clampNumber)(options.flushAt || DEFAULT_FLUSH_AT, MAX_FLUSH_AT, MIN_FLUSH_AT);
147
149
  this.maxQueueSize = (0, utils_1.clampNumber)(options.maxQueueSize || DEFAULT_QUEUE_SIZE, MAX_QUEUE_SIZE, MIN_QUEUE_SIZE);
@@ -175,13 +177,31 @@ var EventQueue = /** @class */ (function () {
175
177
  });
176
178
  });
177
179
  };
180
+ /**
181
+ * Drop all queued data and cancel the flush timer. Called on consent
182
+ * withdrawal / SDK teardown so nothing buffered can be sent later.
183
+ */
184
+ EventQueue.prototype.clear = function () {
185
+ if (this.timer) {
186
+ clearTimeout(this.timer);
187
+ this.timer = null;
188
+ }
189
+ this.queue = [];
190
+ this.queueByteSize = 0;
191
+ this.payloadHashes.clear();
192
+ };
178
193
  EventQueue.prototype.enqueue = function (event, callback) {
179
194
  return __awaiter(this, void 0, void 0, function () {
180
- var message_id, hasReachedFlushAt, hasReachedQueueSize;
195
+ var message_id, queueItem, hasReachedFlushAt, hasReachedQueueSize;
181
196
  return __generator(this, function (_a) {
182
197
  switch (_a.label) {
183
198
  case 0:
184
199
  callback = callback || noop;
200
+ // Refuse to buffer anything once consent is withdrawn.
201
+ if (this.canSend && !this.canSend()) {
202
+ this.clear();
203
+ return [2 /*return*/];
204
+ }
185
205
  return [4 /*yield*/, this.generateMessageId(event)];
186
206
  case 1:
187
207
  message_id = _a.sent();
@@ -190,10 +210,18 @@ var EventQueue = /** @class */ (function () {
190
210
  logger_1.logger.warn("Event already enqueued, try again after ".concat((0, utils_1.millisecondsToSecond)(this.flushIntervalMs), " seconds."));
191
211
  return [2 /*return*/];
192
212
  }
193
- this.queue.push({
213
+ queueItem = {
194
214
  message: __assign(__assign({}, event), { message_id: message_id }),
195
215
  callback: callback,
196
- });
216
+ byteSize: 0,
217
+ };
218
+ // Measure once here (message only — JSON.stringify drops the
219
+ // callback function anyway), then track the total incrementally.
220
+ queueItem.byteSize = JSON.stringify({
221
+ message: queueItem.message,
222
+ }).length;
223
+ this.queue.push(queueItem);
224
+ this.queueByteSize += queueItem.byteSize;
197
225
  logger_1.logger.log("Event enqueued: ".concat((0, utils_1.getActionDescriptor)(event.type, event.properties)));
198
226
  if (!this.flushed) {
199
227
  this.flushed = true;
@@ -201,8 +229,7 @@ var EventQueue = /** @class */ (function () {
201
229
  return [2 /*return*/];
202
230
  }
203
231
  hasReachedFlushAt = this.queue.length >= this.flushAt;
204
- hasReachedQueueSize = this.queue.reduce(function (acc, item) { return acc + JSON.stringify(item).length; }, 0) >=
205
- this.maxQueueSize;
232
+ hasReachedQueueSize = this.queueByteSize >= this.maxQueueSize;
206
233
  if (hasReachedFlushAt || hasReachedQueueSize) {
207
234
  this.flush();
208
235
  return [2 /*return*/];
@@ -228,6 +255,13 @@ var EventQueue = /** @class */ (function () {
228
255
  clearTimeout(this.timer);
229
256
  this.timer = null;
230
257
  }
258
+ // Final consent gate: a timer/pagehide flush may have been scheduled
259
+ // before opt-out. Drop everything rather than send post-withdrawal.
260
+ if (this.canSend && !this.canSend()) {
261
+ this.clear();
262
+ callback();
263
+ return [2 /*return*/, Promise.resolve()];
264
+ }
231
265
  if (!this.queue.length) {
232
266
  callback();
233
267
  return [2 /*return*/, Promise.resolve()];
@@ -241,11 +275,17 @@ var EventQueue = /** @class */ (function () {
241
275
  case 2:
242
276
  items = this.queue.splice(0, drainAll ? this.queue.length : this.flushAt);
243
277
  // Only remove hashes for flushed items so duplicate detection remains
244
- // active for events still in the queue.
278
+ // active for events still in the queue. Also decrement the running
279
+ // byte total by exactly what left the queue.
245
280
  for (_i = 0, items_1 = items; _i < items_1.length; _i++) {
246
281
  item = items_1[_i];
247
282
  this.payloadHashes.delete(item.message.message_id);
283
+ this.queueByteSize -= item.byteSize;
248
284
  }
285
+ // Re-anchor to the exact invariant when the queue empties, so any
286
+ // accumulated drift can never wedge the size gate.
287
+ if (this.queue.length === 0)
288
+ this.queueByteSize = 0;
249
289
  sentAt = new Date().toISOString();
250
290
  data = items.map(function (item) { return (__assign(__assign({}, item.message), { sent_at: sentAt })); });
251
291
  batches = this.splitIntoBatches(items, data);
@@ -348,6 +388,12 @@ var EventQueue = /** @class */ (function () {
348
388
  case 1:
349
389
  if (!(_i < batches_1.length)) return [3 /*break*/, 6];
350
390
  batch = batches_1[_i];
391
+ // Consent can be withdrawn while a flush is already in flight:
392
+ // batches were spliced before opt-out, and split batches / retry
393
+ // backoff span seconds. Re-check before every send and abandon
394
+ // the remaining batches if consent was revoked mid-flush.
395
+ if (this.canSend && !this.canSend())
396
+ return [3 /*break*/, 6];
351
397
  _a.label = 2;
352
398
  case 2:
353
399
  _a.trys.push([2, 4, , 5]);
@@ -2,5 +2,6 @@ import { IFormoEvent } from "../types";
2
2
  export interface IEventQueue {
3
3
  enqueue(event: IFormoEvent, callback?: (...args: any) => void): Promise<void>;
4
4
  flush(callback?: (...args: any) => void): Promise<any>;
5
+ clear(): void;
5
6
  }
6
7
  //# sourceMappingURL=type.d.ts.map
@@ -5,9 +5,21 @@
5
5
  * Handles tracking of detected wallets and identified wallet-address pairs
6
6
  * using cookies to maintain state across page loads within a session.
7
7
  */
8
+ var __assign = (this && this.__assign) || function () {
9
+ __assign = Object.assign || function(t) {
10
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
11
+ s = arguments[i];
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
13
+ t[p] = s[p];
14
+ }
15
+ return t;
16
+ };
17
+ return __assign.apply(this, arguments);
18
+ };
8
19
  Object.defineProperty(exports, "__esModule", { value: true });
9
20
  exports.FormoAnalyticsSession = exports.SESSION_WALLET_IDENTIFIED_KEY = exports.SESSION_WALLET_DETECTED_KEY = void 0;
10
21
  var storage_1 = require("../storage");
22
+ var cookiePolicy_1 = require("../storage/cookiePolicy");
11
23
  var logger_1 = require("../logger");
12
24
  /**
13
25
  * Cookie keys for session tracking
@@ -66,11 +78,9 @@ var FormoAnalyticsSession = /** @class */ (function () {
66
78
  if (rdnses.length > MAX_SESSION_ENTRIES) {
67
79
  rdnses.splice(0, rdnses.length - MAX_SESSION_ENTRIES);
68
80
  }
69
- (0, storage_1.cookie)().set(exports.SESSION_WALLET_DETECTED_KEY, rdnses.join(","), {
81
+ (0, storage_1.cookie)().set(exports.SESSION_WALLET_DETECTED_KEY, rdnses.join(","), __assign({
70
82
  // Expires by the end of the day
71
- expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
72
- path: "/",
73
- });
83
+ expires: new Date(Date.now() + 86400 * 1000).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
74
84
  }
75
85
  };
76
86
  /**
@@ -110,11 +120,9 @@ var FormoAnalyticsSession = /** @class */ (function () {
110
120
  identifiedWallets.splice(0, identifiedWallets.length - MAX_SESSION_ENTRIES);
111
121
  }
112
122
  var newValue = identifiedWallets.join(",");
113
- (0, storage_1.cookie)().set(exports.SESSION_WALLET_IDENTIFIED_KEY, newValue, {
123
+ (0, storage_1.cookie)().set(exports.SESSION_WALLET_IDENTIFIED_KEY, newValue, __assign({
114
124
  // Expires by the end of the day
115
- expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
116
- path: "/",
117
- });
125
+ expires: new Date(Date.now() + 86400 * 1000).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
118
126
  logger_1.logger.debug("Session: Marked wallet as identified", {
119
127
  identifiedKey: identifiedKey,
120
128
  hasRdns: !!rdns,
@@ -7,4 +7,20 @@
7
7
  * @param crossSubdomain Whether cookies should be shared across subdomains.
8
8
  */
9
9
  export declare function getIdentityCookieDomain(crossSubdomain?: boolean): string;
10
+ /**
11
+ * Security attributes for identity/session cookies (user-id,
12
+ * active-wallet, wallet-detected/identified).
13
+ *
14
+ * - `sameSite: "lax"` — first-party analytics identity. Lax keeps the
15
+ * cookie on top-level navigations (so attribution survives a click-in
16
+ * from another site) while blocking it on cross-site subrequests.
17
+ * Strict would silently drop identity on inbound navigation.
18
+ * - `secure` — only on HTTPS. Setting Secure over plain HTTP (local dev)
19
+ * makes the browser reject the cookie outright, so it must be
20
+ * conditional.
21
+ */
22
+ export declare function getIdentityCookieSecurity(): {
23
+ sameSite: "lax";
24
+ secure: boolean;
25
+ };
10
26
  //# sourceMappingURL=cookiePolicy.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getIdentityCookieDomain = getIdentityCookieDomain;
4
+ exports.getIdentityCookieSecurity = getIdentityCookieSecurity;
4
5
  /**
5
6
  * Cookie domain policy — centralizes the decision of whether identity
6
7
  * cookies should be host-scoped or apex-scoped.
@@ -24,4 +25,22 @@ function getIdentityCookieDomain(crossSubdomain) {
24
25
  var domain = (0, domain_1.getApexDomain)();
25
26
  return domain ? ".".concat(domain) : "";
26
27
  }
28
+ /**
29
+ * Security attributes for identity/session cookies (user-id,
30
+ * active-wallet, wallet-detected/identified).
31
+ *
32
+ * - `sameSite: "lax"` — first-party analytics identity. Lax keeps the
33
+ * cookie on top-level navigations (so attribution survives a click-in
34
+ * from another site) while blocking it on cross-site subrequests.
35
+ * Strict would silently drop identity on inbound navigation.
36
+ * - `secure` — only on HTTPS. Setting Secure over plain HTTP (local dev)
37
+ * makes the browser reject the cookie outright, so it must be
38
+ * conditional.
39
+ */
40
+ function getIdentityCookieSecurity() {
41
+ var _a;
42
+ var isHttps = typeof window !== "undefined" &&
43
+ ((_a = window.location) === null || _a === void 0 ? void 0 : _a.protocol) === "https:";
44
+ return { sameSite: "lax", secure: isHttps };
45
+ }
27
46
  //# sourceMappingURL=cookiePolicy.js.map
@@ -40,7 +40,6 @@ export interface IFormoAnalytics {
40
40
  chainId?: ChainID;
41
41
  address: Address;
42
42
  message: string;
43
- signatureHash?: string;
44
43
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
45
44
  transaction(params: {
46
45
  status: TransactionStatus;
@@ -76,7 +76,6 @@ export interface SignatureAPIEvent {
76
76
  chainId: ChainID;
77
77
  address: Address;
78
78
  message: string;
79
- signatureHash?: string;
80
79
  }
81
80
  export interface ConnectAPIEvent {
82
81
  type: "connect";
@@ -1,2 +1,2 @@
1
- export declare const version = "1.29.1";
1
+ export declare const version = "1.30.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // This file is auto-generated by scripts/update-version.js during npm version
5
5
  // Do not edit manually - it will be overwritten
6
- exports.version = '1.29.1';
6
+ exports.version = '1.30.0';
7
7
  //# sourceMappingURL=version.js.map