@cross-deck/web 1.11.1 → 1.12.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "generatedAt": "2026-07-23T05:41:37.690Z",
3
+ "generatedAt": "2026-07-24T12:01:24.353Z",
4
4
  "sdk": "@cross-deck/web",
5
5
  "codes": [
6
6
  {
package/dist/index.cjs CHANGED
@@ -104,7 +104,7 @@ function typeMapForStatus(status) {
104
104
  }
105
105
 
106
106
  // src/_version.ts
107
- var SDK_VERSION = "1.11.1";
107
+ var SDK_VERSION = "1.12.1";
108
108
  var SDK_NAME = "@cross-deck/web";
109
109
 
110
110
  // src/http.ts
@@ -1594,6 +1594,7 @@ var AutoTracker = class {
1594
1594
  this.pageviewId = null;
1595
1595
  this.storage = opts?.storage ?? null;
1596
1596
  this.sessionKey = opts?.storageKey ?? SESSION_STORAGE_KEY;
1597
+ this.cookieStorage = opts?.cookieStorage ?? null;
1597
1598
  }
1598
1599
  install() {
1599
1600
  if (!isBrowserSafe()) return;
@@ -1745,9 +1746,74 @@ var AutoTracker = class {
1745
1746
  lastActivityAt: now,
1746
1747
  hiddenAt: null,
1747
1748
  endedSent: false,
1748
- acquisition: captureAcquisition()
1749
+ acquisition: this.resolveAcquisition()
1749
1750
  };
1750
1751
  }
1752
+ /**
1753
+ * Session acquisition, with FIRST-TOUCH survival across sessions.
1754
+ *
1755
+ * WHY: `captureAcquisition()` reads the CURRENT page. That is correct for a
1756
+ * fresh arrival, but a session boundary (30-min idle, or a return visit next
1757
+ * week) starts a new session — and a visitor returning directly has no utm_*
1758
+ * and no click id, so the acquisition that actually WON them was silently
1759
+ * replaced with empty. Someone who arrives from LinkedIn on Monday, returns
1760
+ * direct on Tuesday and signs up on Wednesday converted with no source at all.
1761
+ *
1762
+ * That is exactly the question the product exists to answer — "where did this
1763
+ * PAYING CUSTOMER come from" — so origin has to outlive the session that
1764
+ * captured it. We persist the first touch that carried a real signal and,
1765
+ * whenever a later session arrives with nothing, fall back to it.
1766
+ *
1767
+ * Deliberately NOT last-touch-wins: a real new campaign click overwrites
1768
+ * nothing, it simply records a newer touch for that session while the stored
1769
+ * first touch stays put. Write-once by design.
1770
+ */
1771
+ resolveAcquisition() {
1772
+ const current = captureAcquisition();
1773
+ if (!this.storage && !this.cookieStorage) return current;
1774
+ const key = `${this.sessionKey}_origin_first`;
1775
+ const readFirstTouch = () => {
1776
+ try {
1777
+ const v = this.storage?.getItem(key);
1778
+ if (v) return v;
1779
+ } catch {
1780
+ }
1781
+ try {
1782
+ return this.cookieStorage?.getItem(key) ?? null;
1783
+ } catch {
1784
+ return null;
1785
+ }
1786
+ };
1787
+ const writeFirstTouch = (v) => {
1788
+ try {
1789
+ this.storage?.setItem(key, v);
1790
+ } catch {
1791
+ }
1792
+ try {
1793
+ this.cookieStorage?.setItem(key, v);
1794
+ } catch {
1795
+ }
1796
+ };
1797
+ const hasSignal = (a) => Boolean(a.utm_source || a.utm_medium || a.utm_campaign || a.utm_content || a.utm_term || a.gclid || a.fbclid || a.msclkid || a.ttclid || a.li_fat_id || a.twclid);
1798
+ try {
1799
+ if (hasSignal(current)) {
1800
+ if (readFirstTouch() === null) writeFirstTouch(JSON.stringify(current));
1801
+ return current;
1802
+ }
1803
+ const raw = readFirstTouch();
1804
+ if (raw) {
1805
+ const stored = JSON.parse(raw);
1806
+ return {
1807
+ ...EMPTY_ACQUISITION,
1808
+ ...stored,
1809
+ // Referrer always describes THIS visit, never the remembered one.
1810
+ referrer: current.referrer
1811
+ };
1812
+ }
1813
+ } catch {
1814
+ }
1815
+ return current;
1816
+ }
1751
1817
  /**
1752
1818
  * Read the persisted session continuity record. Returns null on no
1753
1819
  * storage, no record, malformed JSON, or a record missing its required
@@ -2743,7 +2809,7 @@ var BreadcrumbBuffer = class {
2743
2809
 
2744
2810
  // src/_diagnostic-telemetry.ts
2745
2811
  var DIAGNOSTIC_TELEMETRY_ENDPOINT = "https://api.cross-deck.com/v1/events";
2746
- var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_9490e7aa029c432abf";
2812
+ var DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY = "cd_pub_live_eea2f84dbb834a45b7";
2747
2813
  function isDiagnosticTelemetryEnabled() {
2748
2814
  return !DIAGNOSTIC_TELEMETRY_PUBLISHABLE_KEY.startsWith(
2749
2815
  "cd_pub_RELIABILITY_PLACEHOLDER"
@@ -4794,7 +4860,15 @@ var CrossdeckClient = class {
4794
4860
  // a visit survives full-page navigations (multi-page sites
4795
4861
  // re-install the SDK on every page) and honours the same consent
4796
4862
  // posture — MemoryStorage when identity persistence is off.
4797
- { storage: effectiveStorage, storageKey: opts.storagePrefix + "session" }
4863
+ // cookieStorage is the SAME registrable-domain cookie identity uses.
4864
+ // First-touch origin rides it so the campaign that won a visitor on the
4865
+ // marketing site is still known when they land on the app subdomain —
4866
+ // localStorage alone is per-origin and would lose it at exactly that hop.
4867
+ {
4868
+ storage: effectiveStorage,
4869
+ storageKey: opts.storagePrefix + "session",
4870
+ cookieStorage: cookieStore ?? void 0
4871
+ }
4798
4872
  );
4799
4873
  this.state.autoTracker = tracker;
4800
4874
  tracker.install();
@@ -5640,7 +5714,7 @@ var CrossdeckClient = class {
5640
5714
  }
5641
5715
  }
5642
5716
  this.state.autoTracker?.uninstall();
5643
- this.state.identity.reset();
5717
+ if (this.state.developerUserId) this.state.identity.reset();
5644
5718
  bridgeReadCost({ actor: void 0 });
5645
5719
  this.state.entitlements.clearAll();
5646
5720
  this.state.events.reset();
@@ -5874,8 +5948,8 @@ function installUnloadFlush(onUnload) {
5874
5948
  }
5875
5949
 
5876
5950
  // src/_contracts-bundled.ts
5877
- var BUNDLED_IN = "@cross-deck/web@1.8.0";
5878
- var SDK_VERSION2 = "1.8.0";
5951
+ var BUNDLED_IN = "@cross-deck/web@1.12.0";
5952
+ var SDK_VERSION2 = "1.12.0";
5879
5953
  var BUNDLED_CONTRACTS = Object.freeze([
5880
5954
  {
5881
5955
  "id": "contract-failed-payload-schema-lock",
@@ -5997,7 +6071,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
5997
6071
  "legal/security/index.html#diagnostic",
5998
6072
  "legal/sdk-data/index.html#b-diagnostic"
5999
6073
  ],
6000
- "bundledIn": "@cross-deck/web@1.8.0",
6074
+ "bundledIn": "@cross-deck/web@1.12.0",
6001
6075
  "runtimeVerified": true
6002
6076
  },
6003
6077
  {
@@ -6037,7 +6111,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6037
6111
  ],
6038
6112
  "registeredAt": "2026-05-26",
6039
6113
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
6040
- "bundledIn": "@cross-deck/web@1.8.0",
6114
+ "bundledIn": "@cross-deck/web@1.12.0",
6041
6115
  "runtimeVerified": true
6042
6116
  },
6043
6117
  {
@@ -6083,7 +6157,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6083
6157
  ],
6084
6158
  "registeredAt": "2026-05-26",
6085
6159
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
6086
- "bundledIn": "@cross-deck/web@1.8.0",
6160
+ "bundledIn": "@cross-deck/web@1.12.0",
6087
6161
  "runtimeVerified": true
6088
6162
  },
6089
6163
  {
@@ -6189,7 +6263,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6189
6263
  ],
6190
6264
  "registeredAt": "2026-05-26",
6191
6265
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
6192
- "bundledIn": "@cross-deck/web@1.8.0",
6266
+ "bundledIn": "@cross-deck/web@1.12.0",
6193
6267
  "runtimeVerified": true
6194
6268
  },
6195
6269
  {
@@ -6217,7 +6291,64 @@ var BUNDLED_CONTRACTS = Object.freeze([
6217
6291
  ],
6218
6292
  "registeredAt": "2026-05-26",
6219
6293
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
6220
- "bundledIn": "@cross-deck/web@1.8.0",
6294
+ "bundledIn": "@cross-deck/web@1.12.0",
6295
+ "runtimeVerified": false
6296
+ },
6297
+ {
6298
+ "id": "invalid-input-rejected-natively",
6299
+ "pillar": "errors",
6300
+ "status": "enforced",
6301
+ "claim": "No public SDK API ever crashes the host app, and invalid input never reaches the wire. Invalid input (empty event name, empty userId, out-of-range config such as a non-positive breadcrumb capacity, NaN/Infinity/oversize/cyclic property values) is rejected at the call site WITHOUT a fatal trap. The signalling IDIOM is per-language and intentionally NOT uniform: Web, Node, and React Native THROW a typed CrossdeckError synchronously (code missing_event_name / missing_user_id / invalid_request_error) \u2014 a normal, catchable JavaScript convention where an uncaught throw logs and the app continues; Swift DROPS with a debug-log signal (track_dropped / identify_dropped) to match its non-throwing fire-and-forget surface, and exposes the throwing equivalent only via identifyAndWait(userId:). What is UNIFORM is the invariant, not the mechanism: every SDK rejects the same inputs, no public fire-and-forget API contains a fatalError / assertionFailure / precondition reachable from customer input, and no rejected input is enqueued or transmitted. Swift additionally proves this in BOTH debug and release configuration, because precondition fires under -O while assertionFailure does not. The bug class this contract closes was never the per-language difference \u2014 it was the UNDECLARED difference: each SDK's public API documentation must state its own semantics explicitly (TS docs: 'throws on empty name'; Swift docs: 'drops and logs').",
6302
+ "appliesTo": [
6303
+ "web",
6304
+ "node",
6305
+ "react-native",
6306
+ "swift"
6307
+ ],
6308
+ "codeRef": [
6309
+ "sdks/web/src/crossdeck.ts",
6310
+ "sdks/node/src/crossdeck-server.ts",
6311
+ "sdks/react-native/src/crossdeck.ts",
6312
+ "sdks/swift/Sources/Crossdeck/Crossdeck.swift",
6313
+ "sdks/swift/Sources/Crossdeck/Breadcrumbs.swift"
6314
+ ],
6315
+ "testRef": [
6316
+ {
6317
+ "file": "sdks/web/tests/crossdeck.test.ts",
6318
+ "name": "track with empty name throws synchronously"
6319
+ },
6320
+ {
6321
+ "file": "sdks/web/tests/crossdeck.test.ts",
6322
+ "name": "rejects empty userId"
6323
+ },
6324
+ {
6325
+ "file": "sdks/node/tests/crossdeck-server.test.ts",
6326
+ "name": "track() throws CrossdeckError with code 'missing_event_name' when event name is empty"
6327
+ },
6328
+ {
6329
+ "file": "sdks/react-native/tests/crossdeck.test.ts",
6330
+ "name": "track('') throws CrossdeckError(missing_event_name) synchronously"
6331
+ },
6332
+ {
6333
+ "file": "sdks/react-native/tests/crossdeck.test.ts",
6334
+ "name": "identify('') rejects with CrossdeckError(missing_user_id)"
6335
+ },
6336
+ {
6337
+ "file": "sdks/swift/Tests/CrossdeckTests/CrossdeckPublicAPITests.swift",
6338
+ "name": "test_track_dropsEmptyName"
6339
+ },
6340
+ {
6341
+ "file": "sdks/swift/Tests/CrossdeckTests/CrossdeckPublicAPITests.swift",
6342
+ "name": "test_identifyAndWait_rejectsEmptyId"
6343
+ },
6344
+ {
6345
+ "file": "sdks/swift/Tests/CrossdeckTests/PublicAPIInputSafetyTests.swift",
6346
+ "name": "test_start_withZeroBreadcrumbCapacity_doesNotTrap"
6347
+ }
6348
+ ],
6349
+ "registeredAt": "2026-06-11",
6350
+ "firstRegisteredIn": "swift trap-on-input class fix \u2014 first machine-tested Swift release",
6351
+ "bundledIn": "@cross-deck/web@1.12.0",
6221
6352
  "runtimeVerified": false
6222
6353
  },
6223
6354
  {
@@ -6297,7 +6428,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6297
6428
  ],
6298
6429
  "registeredAt": "2026-05-26",
6299
6430
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
6300
- "bundledIn": "@cross-deck/web@1.8.0",
6431
+ "bundledIn": "@cross-deck/web@1.12.0",
6301
6432
  "runtimeVerified": true
6302
6433
  },
6303
6434
  {
@@ -6343,7 +6474,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6343
6474
  ],
6344
6475
  "registeredAt": "2026-05-26",
6345
6476
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
6346
- "bundledIn": "@cross-deck/web@1.8.0",
6477
+ "bundledIn": "@cross-deck/web@1.12.0",
6347
6478
  "runtimeVerified": true
6348
6479
  },
6349
6480
  {
@@ -6385,7 +6516,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6385
6516
  ],
6386
6517
  "registeredAt": "2026-05-26",
6387
6518
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.2",
6388
- "bundledIn": "@cross-deck/web@1.8.0",
6519
+ "bundledIn": "@cross-deck/web@1.12.0",
6389
6520
  "runtimeVerified": true
6390
6521
  },
6391
6522
  {
@@ -6419,7 +6550,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
6419
6550
  ],
6420
6551
  "registeredAt": "2026-05-26",
6421
6552
  "firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
6422
- "bundledIn": "@cross-deck/web@1.8.0",
6553
+ "bundledIn": "@cross-deck/web@1.12.0",
6423
6554
  "runtimeVerified": false
6424
6555
  }
6425
6556
  ]);