@cross-deck/web 1.2.0 → 1.3.1

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/index.d.mts CHANGED
@@ -662,6 +662,25 @@ declare class MemoryStorage implements KeyValueStorage {
662
662
  removeItem(key: string): void;
663
663
  }
664
664
 
665
+ /**
666
+ * SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
667
+ *
668
+ * Single source of truth: the `version` field in this package's
669
+ * package.json. The sync script writes this file so that
670
+ * `SDK_VERSION` is a plain TypeScript literal at runtime — no
671
+ * runtime JSON-import gotcha (Node ESM requires
672
+ * `with { type: "json" }` to import JSON as ESM, and the published
673
+ * dist file would otherwise fail to load).
674
+ *
675
+ * Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
676
+ * CI gate) flags this file when it falls out of sync with package.json.
677
+ * Bumping `package.json` without re-running the sync script fails CI.
678
+ *
679
+ * Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
680
+ */
681
+ declare const SDK_VERSION = "1.3.0";
682
+ declare const SDK_NAME = "@cross-deck/web";
683
+
665
684
  /**
666
685
  * HTTP transport for the SDK. Single fetch wrapper used by every endpoint
667
686
  * call. Adds the Bearer token and SDK version header, parses responses,
@@ -670,8 +689,7 @@ declare class MemoryStorage implements KeyValueStorage {
670
689
  * Uses platform-native fetch (browser + Node 18+). No axios, no isomorphic-
671
690
  * fetch shim, no transitive deps.
672
691
  */
673
- declare const SDK_NAME = "@cross-deck/web";
674
- declare const SDK_VERSION = "1.1.0";
692
+
675
693
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
676
694
 
677
695
  /**
package/dist/index.d.ts CHANGED
@@ -662,6 +662,25 @@ declare class MemoryStorage implements KeyValueStorage {
662
662
  removeItem(key: string): void;
663
663
  }
664
664
 
665
+ /**
666
+ * SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
667
+ *
668
+ * Single source of truth: the `version` field in this package's
669
+ * package.json. The sync script writes this file so that
670
+ * `SDK_VERSION` is a plain TypeScript literal at runtime — no
671
+ * runtime JSON-import gotcha (Node ESM requires
672
+ * `with { type: "json" }` to import JSON as ESM, and the published
673
+ * dist file would otherwise fail to load).
674
+ *
675
+ * Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
676
+ * CI gate) flags this file when it falls out of sync with package.json.
677
+ * Bumping `package.json` without re-running the sync script fails CI.
678
+ *
679
+ * Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
680
+ */
681
+ declare const SDK_VERSION = "1.3.0";
682
+ declare const SDK_NAME = "@cross-deck/web";
683
+
665
684
  /**
666
685
  * HTTP transport for the SDK. Single fetch wrapper used by every endpoint
667
686
  * call. Adds the Bearer token and SDK version header, parses responses,
@@ -670,8 +689,7 @@ declare class MemoryStorage implements KeyValueStorage {
670
689
  * Uses platform-native fetch (browser + Node 18+). No axios, no isomorphic-
671
690
  * fetch shim, no transitive deps.
672
691
  */
673
- declare const SDK_NAME = "@cross-deck/web";
674
- declare const SDK_VERSION = "1.1.0";
692
+
675
693
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
676
694
 
677
695
  /**
package/dist/index.mjs CHANGED
@@ -63,9 +63,11 @@ function typeMapForStatus(status) {
63
63
  return "internal_error";
64
64
  }
65
65
 
66
- // src/http.ts
66
+ // src/_version.ts
67
+ var SDK_VERSION = "1.3.0";
67
68
  var SDK_NAME = "@cross-deck/web";
68
- var SDK_VERSION = "1.1.0";
69
+
70
+ // src/http.ts
69
71
  var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
70
72
  var DEFAULT_TIMEOUT_MS = 15e3;
71
73
  var HttpClient = class {
@@ -496,8 +498,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
496
498
  const safeAttempts = Math.min(attempts, 30);
497
499
  const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
498
500
  const jittered = ceiling * random();
499
- if (retryAfterMs !== void 0 && retryAfterMs > jittered) {
500
- return Math.min(max, retryAfterMs);
501
+ if (retryAfterMs !== void 0) {
502
+ const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
503
+ const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
504
+ if (honoured > jittered) return honoured;
501
505
  }
502
506
  return Math.max(0, Math.round(jittered));
503
507
  }
@@ -532,6 +536,27 @@ var EventQueue = class {
532
536
  constructor(cfg) {
533
537
  this.cfg = cfg;
534
538
  this.buffer = [];
539
+ /**
540
+ * In-flight events that have been spliced from `buffer` for the
541
+ * current batch but haven't yet been confirmed (success or permanent
542
+ * failure). On a retry-driven re-flush we re-use this slot alongside
543
+ * `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
544
+ * across retries of the SAME logical batch.
545
+ *
546
+ * Pre-fix the splice cleared the buffer AND we immediately
547
+ * `persistent.save(empty)` BEFORE awaiting the network call — a
548
+ * crash mid-flight wiped the persisted blob and the batch was lost
549
+ * with no replay on next boot. Now the persisted blob always carries
550
+ * `[...pendingBatch, ...buffer]` so the in-flight events survive
551
+ * until the server confirms them.
552
+ *
553
+ * Pre-fix every retry attempt also minted a NEW batchId via
554
+ * `splice + mintBatchId`, defeating the backend's
555
+ * `Idempotency-Key` dedup. Reuse via this slot brings web in
556
+ * lockstep with node (which already had the field).
557
+ */
558
+ this.pendingBatch = null;
559
+ this.pendingBatchId = null;
535
560
  this.dropped = 0;
536
561
  this.inFlight = 0;
537
562
  this.lastFlushAt = 0;
@@ -564,7 +589,7 @@ var EventQueue = class {
564
589
  this.cfg.onDrop?.(overflow);
565
590
  }
566
591
  this.cfg.onBufferChange?.(this.buffer.length);
567
- this.persistent?.save(this.buffer);
592
+ this.persistAll();
568
593
  if (this.buffer.length >= this.cfg.batchSize) {
569
594
  void this.flush();
570
595
  } else {
@@ -573,22 +598,44 @@ var EventQueue = class {
573
598
  }
574
599
  /**
575
600
  * Flush the buffer to /v1/events. Resolves when the network call
576
- * completes (success or failure). On failure, events stay in the
577
- * buffer for the next scheduled retry.
601
+ * completes (success or failure). On a retryable failure the batch
602
+ * stays in `pendingBatch` for the next scheduled retry — the SAME
603
+ * batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
604
+ *
605
+ * Three terminal states from one call:
606
+ * - 2xx success: pendingBatch cleared, persisted state collapses to
607
+ * just `buffer` (any new events that arrived during in-flight).
608
+ * - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
609
+ * counter increments, a `permanent_failure` signal fires. The
610
+ * server is telling us our request is malformed / our key is
611
+ * revoked / we lack permission — retrying with the same key
612
+ * forever just grows the queue while the customer thinks events
613
+ * are landing.
614
+ * - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
615
+ * schedules a retry; the next `flush()` re-uses both.
578
616
  *
579
617
  * `options.keepalive` marks the underlying fetch as keepalive so the
580
- * browser keeps the request alive past page unload. Use this for
581
- * terminal flushes (pagehide / visibilitychange→hidden / beforeunload).
618
+ * browser keeps the request alive past page unload. Use for terminal
619
+ * flushes (pagehide / visibilitychange→hidden / beforeunload).
582
620
  */
583
621
  async flush(options = {}) {
584
- if (this.buffer.length === 0) return null;
622
+ let batch;
623
+ let batchId;
624
+ if (this.pendingBatch !== null && this.pendingBatchId !== null) {
625
+ batch = this.pendingBatch;
626
+ batchId = this.pendingBatchId;
627
+ } else {
628
+ if (this.buffer.length === 0) return null;
629
+ batch = this.buffer.splice(0);
630
+ batchId = this.mintBatchId();
631
+ this.pendingBatch = batch;
632
+ this.pendingBatchId = batchId;
633
+ this.inFlight += batch.length;
634
+ this.cfg.onBufferChange?.(this.buffer.length);
635
+ this.persistAll();
636
+ }
585
637
  this.cancelTimerIfSet();
586
638
  this.nextRetryAt = null;
587
- const batch = this.buffer.splice(0);
588
- const batchId = this.mintBatchId();
589
- this.inFlight += batch.length;
590
- this.persistent?.save(this.buffer);
591
- this.cfg.onBufferChange?.(this.buffer.length);
592
639
  try {
593
640
  const env = this.cfg.envelope();
594
641
  const result = await this.cfg.http.request("POST", "/events", {
@@ -607,20 +654,33 @@ var EventQueue = class {
607
654
  this.lastFlushAt = Date.now();
608
655
  this.lastError = null;
609
656
  this.inFlight -= batch.length;
657
+ this.pendingBatch = null;
658
+ this.pendingBatchId = null;
610
659
  this.retry.recordSuccess();
611
- this.persistent?.save(this.buffer);
660
+ this.persistAll();
612
661
  if (!this.firstFlushFired) {
613
662
  this.firstFlushFired = true;
614
663
  this.cfg.onFirstFlushSuccess?.();
615
664
  }
616
665
  return result;
617
666
  } catch (err) {
618
- this.buffer.unshift(...batch);
619
- this.inFlight -= batch.length;
620
667
  const message = err instanceof Error ? err.message : String(err);
621
668
  this.lastError = message;
622
- this.persistent?.save(this.buffer);
623
- this.cfg.onBufferChange?.(this.buffer.length);
669
+ if (isPermanent4xx(err)) {
670
+ const droppedCount = batch.length;
671
+ this.pendingBatch = null;
672
+ this.pendingBatchId = null;
673
+ this.inFlight -= droppedCount;
674
+ this.dropped += droppedCount;
675
+ this.persistAll();
676
+ this.cfg.onDrop?.(droppedCount);
677
+ this.cfg.onPermanentFailure?.({
678
+ status: err.status ?? 0,
679
+ droppedCount,
680
+ lastError: message
681
+ });
682
+ return null;
683
+ }
624
684
  const retryAfterMs = extractRetryAfterMs(err);
625
685
  const delay = this.retry.nextDelay(retryAfterMs);
626
686
  this.scheduleRetry(delay);
@@ -638,6 +698,8 @@ var EventQueue = class {
638
698
  this.cancelTimerIfSet();
639
699
  this.nextRetryAt = null;
640
700
  this.buffer = [];
701
+ this.pendingBatch = null;
702
+ this.pendingBatchId = null;
641
703
  this.dropped = 0;
642
704
  this.inFlight = 0;
643
705
  this.lastError = null;
@@ -647,6 +709,10 @@ var EventQueue = class {
647
709
  }
648
710
  getStats() {
649
711
  return {
712
+ // `buffered` counts events waiting for their FIRST flush. The
713
+ // in-flight pendingBatch (retrying) is tracked separately via
714
+ // `inFlight` — surfacing both lets diagnostics show "we have
715
+ // events stuck retrying" distinct from "new events arriving".
650
716
  buffered: this.buffer.length,
651
717
  dropped: this.dropped,
652
718
  inFlight: this.inFlight,
@@ -656,6 +722,29 @@ var EventQueue = class {
656
722
  nextRetryAt: this.nextRetryAt
657
723
  };
658
724
  }
725
+ /**
726
+ * The Idempotency-Key of the in-flight pending batch (if any).
727
+ * Exposed for testing the Stripe-style retry-reuse contract.
728
+ * Production callers don't need this.
729
+ */
730
+ get pendingIdempotencyKey() {
731
+ return this.pendingBatchId;
732
+ }
733
+ /**
734
+ * Persist `[...pendingBatch, ...buffer]` — the full set of
735
+ * not-yet-confirmed events. The next boot rehydrates this exact set
736
+ * into `buffer` and replays. The server dedups via eventId
737
+ * (ReplacingMergeTree on the warehouse side), so re-sending an event
738
+ * that may have already landed is safe.
739
+ */
740
+ persistAll() {
741
+ if (!this.persistent) return;
742
+ if (this.pendingBatch === null) {
743
+ this.persistent.save(this.buffer);
744
+ return;
745
+ }
746
+ this.persistent.save([...this.pendingBatch, ...this.buffer]);
747
+ }
659
748
  // ---------- internal scheduling ----------
660
749
  scheduleIdleFlush() {
661
750
  this.cancelTimerIfSet();
@@ -689,6 +778,14 @@ function extractRetryAfterMs(err) {
689
778
  }
690
779
  return void 0;
691
780
  }
781
+ function isPermanent4xx(err) {
782
+ if (!err || typeof err !== "object") return false;
783
+ const status = err.status;
784
+ if (typeof status !== "number" || !Number.isFinite(status)) return false;
785
+ if (status < 400 || status >= 500) return false;
786
+ if (status === 408 || status === 429) return false;
787
+ return true;
788
+ }
692
789
  function defaultScheduler(fn, ms) {
693
790
  const id = setTimeout(fn, ms);
694
791
  if (typeof id.unref === "function") {
@@ -1030,6 +1127,7 @@ var AutoTracker = class {
1030
1127
  /** Exposed for tests + consumers that want to reset the session manually. */
1031
1128
  resetSession() {
1032
1129
  if (this.session && !this.session.endedSent) this.emitSessionEnd();
1130
+ this.pageviewId = null;
1033
1131
  this.session = this.startNewSession();
1034
1132
  this.emitSessionStart();
1035
1133
  }
@@ -1066,6 +1164,7 @@ var AutoTracker = class {
1066
1164
  const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
1067
1165
  if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
1068
1166
  this.emitSessionEnd();
1167
+ this.pageviewId = null;
1069
1168
  this.session = this.startNewSession();
1070
1169
  this.emitSessionStart();
1071
1170
  } else {
@@ -1439,7 +1538,7 @@ function validateEventProperties(input, options = {}) {
1439
1538
  const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
1440
1539
  const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
1441
1540
  const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
1442
- const seen = /* @__PURE__ */ new WeakSet();
1541
+ const seen = /* @__PURE__ */ new Set();
1443
1542
  const visit = (value, key, depth) => {
1444
1543
  if (depth > maxDepth) {
1445
1544
  warnings.push({ kind: "depth_exceeded", key });
@@ -1527,6 +1626,7 @@ function validateEventProperties(input, options = {}) {
1527
1626
  const result = visit(value[i], `${key}[${i}]`, depth + 1);
1528
1627
  if (result.keep) out.push(result.value);
1529
1628
  }
1629
+ seen.delete(value);
1530
1630
  return { keep: true, value: out };
1531
1631
  }
1532
1632
  if (t === "object") {
@@ -1541,6 +1641,7 @@ function validateEventProperties(input, options = {}) {
1541
1641
  const result = visit(obj[k], `${key}.${k}`, depth + 1);
1542
1642
  if (result.keep) out[k] = result.value;
1543
1643
  }
1644
+ seen.delete(obj);
1544
1645
  return { keep: true, value: out };
1545
1646
  }
1546
1647
  warnings.push({ kind: "non_serialisable", key });
@@ -1883,35 +1984,27 @@ var ConsentManager = class {
1883
1984
  };
1884
1985
  var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
1885
1986
  var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
1886
- var REPLACEMENT_EMAIL = "[email]";
1887
- var REPLACEMENT_CARD = "[card]";
1987
+ var REPLACEMENT_EMAIL = "<email>";
1988
+ var REPLACEMENT_CARD = "<card>";
1888
1989
  function scrubPii(value) {
1889
1990
  if (!value) return value;
1890
- let out = value;
1891
- if (EMAIL_PATTERN.test(out)) {
1892
- out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
1893
- }
1894
- EMAIL_PATTERN.lastIndex = 0;
1895
- if (CARD_PATTERN.test(out)) {
1896
- out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
1897
- }
1898
- CARD_PATTERN.lastIndex = 0;
1899
- return out;
1991
+ return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
1900
1992
  }
1901
1993
  function scrubPiiFromProperties(properties) {
1902
1994
  const out = {};
1903
1995
  for (const k of Object.keys(properties)) {
1904
- const v = properties[k];
1905
- if (typeof v === "string") {
1906
- out[k] = scrubPii(v);
1907
- } else if (Array.isArray(v)) {
1908
- out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
1909
- } else {
1910
- out[k] = v;
1911
- }
1996
+ out[k] = scrubValue(properties[k]);
1912
1997
  }
1913
1998
  return out;
1914
1999
  }
2000
+ function scrubValue(v) {
2001
+ if (typeof v === "string") return scrubPii(v);
2002
+ if (Array.isArray(v)) return v.map(scrubValue);
2003
+ if (v && typeof v === "object" && v.constructor === Object) {
2004
+ return scrubPiiFromProperties(v);
2005
+ }
2006
+ return v;
2007
+ }
1915
2008
 
1916
2009
  // src/breadcrumbs.ts
1917
2010
  var BreadcrumbBuffer = class {
@@ -2201,16 +2294,18 @@ var ErrorTracker = class {
2201
2294
  const url = typeof input === "string" ? input : input?.url ?? "";
2202
2295
  const method = (init.method || "GET").toUpperCase();
2203
2296
  const start = Date.now();
2204
- this.opts.breadcrumbs.add({
2205
- timestamp: start,
2206
- category: "http",
2207
- message: `${method} ${url}`,
2208
- data: { url, method }
2209
- });
2297
+ if (!isSelfRequest(url, this.opts.selfHostname)) {
2298
+ this.opts.breadcrumbs.add({
2299
+ timestamp: start,
2300
+ category: "http",
2301
+ message: `${method} ${url}`,
2302
+ data: { url, method }
2303
+ });
2304
+ }
2210
2305
  try {
2211
2306
  const response = await origFetch(...args);
2212
2307
  if (response.status >= 500 && this.opts.isConsented()) {
2213
- if (!url.includes("api.cross-deck.com")) {
2308
+ if (!isSelfRequest(url, this.opts.selfHostname)) {
2214
2309
  this.captureHttp({
2215
2310
  url,
2216
2311
  method,
@@ -2259,7 +2354,7 @@ var ErrorTracker = class {
2259
2354
  try {
2260
2355
  if (xhr.status >= 500 && tracker.opts.isConsented()) {
2261
2356
  const url = xhr._cdUrl ?? "";
2262
- if (!url.includes("api.cross-deck.com")) {
2357
+ if (!isSelfRequest(url, tracker.opts.selfHostname)) {
2263
2358
  tracker.captureHttp({
2264
2359
  url,
2265
2360
  method: (xhr._cdMethod ?? "GET").toUpperCase(),
@@ -2419,9 +2514,10 @@ var ErrorTracker = class {
2419
2514
  if (!this.passesSample(err)) return;
2420
2515
  if (!this.passesRateLimit(err)) return;
2421
2516
  let finalErr = err;
2422
- if (this.opts.beforeSend) {
2517
+ const hook = this.opts.beforeSend?.();
2518
+ if (hook) {
2423
2519
  try {
2424
- finalErr = this.opts.beforeSend(err);
2520
+ finalErr = hook(err);
2425
2521
  } catch {
2426
2522
  finalErr = err;
2427
2523
  }
@@ -2603,6 +2699,22 @@ function safeClone(v) {
2603
2699
  function safeStringify2(v) {
2604
2700
  return coerceErrorPayload(v).message;
2605
2701
  }
2702
+ function extractSelfHostname(baseUrl) {
2703
+ if (!baseUrl || typeof baseUrl !== "string") return null;
2704
+ try {
2705
+ return new URL(baseUrl).hostname.toLowerCase();
2706
+ } catch {
2707
+ return null;
2708
+ }
2709
+ }
2710
+ function isSelfRequest(requestUrl, selfHostname) {
2711
+ if (!selfHostname || !requestUrl) return false;
2712
+ try {
2713
+ return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
2714
+ } catch {
2715
+ return false;
2716
+ }
2717
+ }
2606
2718
 
2607
2719
  // src/crossdeck.ts
2608
2720
  var CrossdeckClient = class {
@@ -2619,6 +2731,24 @@ var CrossdeckClient = class {
2619
2731
  * mismatched env fails fast at boot rather than at first event-flush.
2620
2732
  */
2621
2733
  init(options) {
2734
+ if (this.state) {
2735
+ try {
2736
+ this.state.uninstallUnloadFlush?.();
2737
+ } catch {
2738
+ }
2739
+ try {
2740
+ this.state.autoTracker?.uninstall();
2741
+ } catch {
2742
+ }
2743
+ try {
2744
+ this.state.webVitals?.uninstall();
2745
+ } catch {
2746
+ }
2747
+ try {
2748
+ this.state.errors?.uninstall();
2749
+ } catch {
2750
+ }
2751
+ }
2622
2752
  if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
2623
2753
  throw new CrossdeckError({
2624
2754
  type: "configuration_error",
@@ -2726,6 +2856,15 @@ var CrossdeckClient = class {
2726
2856
  `Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
2727
2857
  { ...info }
2728
2858
  );
2859
+ },
2860
+ onPermanentFailure: (info) => {
2861
+ const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
2862
+ console.error(headline);
2863
+ debug.emit(
2864
+ "sdk.flush_permanent_failure",
2865
+ headline,
2866
+ { ...info }
2867
+ );
2729
2868
  }
2730
2869
  });
2731
2870
  const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
@@ -2792,8 +2931,20 @@ var CrossdeckClient = class {
2792
2931
  report: (err) => this.reportError(err),
2793
2932
  getContext: () => ({ ...this.state.errorContext }),
2794
2933
  getTags: () => ({ ...this.state.errorTags }),
2795
- beforeSend: this.state.errorBeforeSend,
2796
- isConsented: () => this.state.consent.errors
2934
+ // GETTER, not a captured value — `setErrorBeforeSend()` mutates
2935
+ // `state.errorBeforeSend` after init() and the tracker MUST
2936
+ // pick up the new hook on the next error. The pre-fix shape
2937
+ // (`beforeSend: this.state!.errorBeforeSend`) snapshotted
2938
+ // `null` at construction and made the customer's PII scrubber
2939
+ // silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
2940
+ beforeSend: () => this.state.errorBeforeSend,
2941
+ isConsented: () => this.state.consent.errors,
2942
+ // Derived from the configured baseUrl at init() time. Used by
2943
+ // the fetch / XHR wrappers to skip captureHttp on Crossdeck's
2944
+ // own requests — pre-fix the skip was hardcoded to
2945
+ // `api.cross-deck.com` and broke for customers on staging /
2946
+ // regional / self-hosted base URLs (recursive capture loop).
2947
+ selfHostname: extractSelfHostname(opts.baseUrl)
2797
2948
  });
2798
2949
  this.state.errors = tracker;
2799
2950
  tracker.install();
@@ -2876,7 +3027,8 @@ var CrossdeckClient = class {
2876
3027
  body
2877
3028
  });
2878
3029
  const priorCdcust = s.identity.crossdeckCustomerId;
2879
- if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId) {
3030
+ const cacheHasEntries = s.entitlements.list().length > 0;
3031
+ if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
2880
3032
  s.entitlements.clear();
2881
3033
  }
2882
3034
  s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
@@ -3318,8 +3470,9 @@ var CrossdeckClient = class {
3318
3470
  message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
3319
3471
  });
3320
3472
  }
3473
+ const rail = input.rail ?? "apple";
3321
3474
  const result = await s.http.request("POST", "/purchases/sync", {
3322
- body: { rail: input.rail ?? "apple", ...input }
3475
+ body: { ...input, rail }
3323
3476
  });
3324
3477
  s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
3325
3478
  s.entitlements.setFromList(result.entitlements);
@@ -3385,6 +3538,8 @@ var CrossdeckClient = class {
3385
3538
  this.state.errorContext = {};
3386
3539
  this.state.errorTags = {};
3387
3540
  this.state.developerUserId = null;
3541
+ this.state.lastServerTime = null;
3542
+ this.state.lastClientTime = null;
3388
3543
  if (this.state.autoTracker) {
3389
3544
  const tracker = new AutoTracker(
3390
3545
  this.state.options.autoTrack,
@@ -3513,7 +3668,9 @@ function isLocalHostname() {
3513
3668
  const hostname = w?.location?.hostname;
3514
3669
  if (!hostname) return false;
3515
3670
  if (hostname === "localhost" || hostname === "127.0.0.1") return true;
3671
+ if (hostname === "0.0.0.0") return true;
3516
3672
  if (hostname === "::1" || hostname === "[::1]") return true;
3673
+ if (/^\[?fe80::/i.test(hostname)) return true;
3517
3674
  if (hostname.endsWith(".local")) return true;
3518
3675
  if (/^10\./.test(hostname)) return true;
3519
3676
  if (/^192\.168\./.test(hostname)) return true;