@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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `@cross-deck/web` will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [1.12.1] — 2026-07-24
6
+
7
+ **Republish of 1.12.0 — release-pipeline only, identical SDK.** 1.12.0 never reached npm: the release gate correctly failed it on the bundle budget, and by the time the budget fix landed the `v1.12.0` tag already existed on the public mirror, which refuses to overwrite a published artefact. Version bumped so the release can be cut cleanly. No SDK code changed — see 1.12.0 below for what actually shipped.
8
+
9
+ ## [1.12.0] — 2026-07-24
10
+
11
+ **Bundle budget raised: core ESM/CJS 62 → 63 KB, UMD 35 → 36 KB (gzipped).** The first-touch origin persistence below adds ~0.5 KB of real code — the write-once store, the dual localStorage/cookie read-write, and the signal test. Core CJS measured 62.47 KB against a 62 KB budget and the UMD build 35.14 KB against 35 KB, so the release gate correctly stopped the publish. The budget is raised deliberately, with this note, per the gate's own rule — not silenced. Crossdeck still ships analytics + revenue + errors in roughly what single-purpose SDKs charge for one.
12
+
13
+ - **First-touch origin now survives the session boundary AND the subdomain hop.** Acquisition (`utm_*`, `referrer`, and the click ids `gclid`/`fbclid`/`msclkid`/`ttclid`/`li_fat_id`/`twclid`) was captured per session off the current URL, so a visitor who arrived from a campaign, returned later with no params, and then signed up converted with **empty** acquisition — the channel that won them was overwritten. And because `localStorage` is per-origin, a visitor acquired on a marketing site reached the app subdomain with no memory of the campaign at all, which is exactly where signup happens. The first touch carrying a real signal (any `utm_*` or click id) is now persisted **write-once** and restored whenever a later session lands with none, and it is written to **both** `localStorage` and the shared registrable-domain cookie — the same dual store as identity — so it crosses `example.com` → `app.example.com`.
14
+ - **`referrer` alone is deliberately not treated as a touch.** Every visit has one; counting it would freeze the first referrer permanently and stop any genuine campaign ever registering. `referrer` always describes the current visit.
15
+ - Not last-touch-wins: a newer campaign is the current touch for its own session, but never overwrites the stored first touch. Consent posture unchanged — with `persistIdentity: false` (or a `MemoryStorage` adapter) nothing is persisted and behaviour falls back to the current page.
16
+
17
+ ## [1.11.2] — 2026-07-23
18
+
19
+ - **Reliability telemetry re-pointed to the live project.** The internal `crossdeck.contract_failed` self-check channel used a retired reliability-workspace key that had begun returning `401 invalid_api_key`, silently dropping failures; it now targets the live "Crossdeck Health Check" project. Internal SDK-health only — never touches your dashboard or your app.
20
+ - **`reset()` no longer re-mints the `anonymousId` when no user was ever identified.** It still clears session state (super-properties, groups, breadcrumbs, clock) but only rotates the anonymous id on a real logout (after `identify()`). This stops the common auth-mirroring wiring `onAuthStateChanged(u => u ? identify() : reset())` from fragmenting one anonymous visitor into a new person on every page load.
21
+
5
22
  ## [1.11.1] — 2026-07-23
6
23
 
7
24
  **Republish of 1.11.0 — release-pipeline fix only, identical SDK.** 1.11.0 never
package/README.md CHANGED
@@ -97,7 +97,18 @@ Every event — auto-tracked and developer-emitted — is enriched with the devi
97
97
 
98
98
  **Cross-subdomain identity (v1.11.0+).** A visitor is **one person across every subdomain** of your site — your marketing site (`example.com`) and your app (`app.example.com`) share one anonymous identity, so first-touch source, journey, and conversion stitch into one timeline instead of splitting at the hop. The SDK scopes its identity cookie to your **registrable domain** by default (`cookieDomain: "auto"`); set an explicit `cookieDomain: ".example.com"` to pin it, or `"none"` for host-only. Cross-*device* identity (same person, phone + laptop) is resolved server-side by `identify(userId)` — this is the browser half.
99
99
 
100
- **Per-session acquisition (v0.6.0+):** when a session starts the SDK reads `window.location.search` and `document.referrer` and captures `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, plus `referrer`. Non-empty values are auto-attached to every subsequent event of that session — first-touch attribution stays pinned to the entry URL even after SPA route changes strip the params away, and now stays pinned across full-page navigations within the session too. A new session (>30 min idle) re-reads the URL.
100
+ **Per-session acquisition (v0.6.0+):** when a session starts the SDK reads `window.location.search` and `document.referrer` and captures `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, plus `referrer`. It also captures the paid-traffic click ids many platforms send *instead of* UTMs — `gclid`, `fbclid`, `msclkid`, `ttclid`, `li_fat_id`, `twclid`. Non-empty values are auto-attached to every subsequent event of that session — attribution stays pinned to the entry URL even after SPA route changes strip the params away, and across full-page navigations within the session.
101
+
102
+ **First-touch origin survives the visit (v1.12.0+).** The touch that *won* a visitor now outlives the session that captured it, and the subdomain hop to your app.
103
+
104
+ Previously a new session re-read the current URL, so a visitor who arrived from a campaign on Monday, returned directly on Tuesday and signed up on Wednesday converted with **empty** acquisition — the channel that actually won them was overwritten by a visit carrying no params. Now:
105
+
106
+ - The **first touch carrying a real signal** (any `utm_*` or click id) is persisted **write-once**. A later session that lands with no params **restores it**, so the conversion still carries its source.
107
+ - It is stored in **both** localStorage and the shared registrable-domain cookie — the same dual store as identity — so origin captured on `example.com` is still known on `app.example.com`. Without the cookie leg, localStorage is per-origin and every acquisition dies at exactly the hop where signup happens.
108
+ - **`referrer` alone is not a touch.** Every visit has one; treating it as a signal would freeze the first referrer forever and stop any genuine campaign registering. `referrer` always describes the *current* visit.
109
+ - Not last-touch-wins: a newer campaign is the current touch for its own session, but the stored first touch is never overwritten.
110
+
111
+ Consent posture is unchanged — with `persistIdentity: false` (or a `MemoryStorage` adapter) nothing is persisted and behaviour falls back to the current page.
101
112
 
102
113
  **Campaign-arrival connect (v1.10.0+):** when a page is reached from a Crossdeck-tagged campaign link, the landing URL carries an opaque `cd_ref` tag (a partner contact id — never an email). On session start the SDK posts it once to the arrival endpoint so the backend can bind this anonymous session to the tagged person and pull their integration record (e.g. HubSpot deals/pipeline) onto their journey — no login required. It's automatic (no method to call), browser-only, fire-and-forget (never throws, never blocks page init), and idempotent server-side. A later real `identify(userId)` still converges the same person.
103
114
 
@@ -240,6 +251,8 @@ Manually send a heartbeat. Called automatically by `init()` unless `autoHeartbea
240
251
 
241
252
  Wipe persisted identity + EVERY per-user entitlement cache slot on this device + queued events. Call on logout. The next session generates a fresh `anonymousId` and starts a clean identity-graph entry. The per-user scope of the cache wipe (introduced v1.4.0) means a shared-device logout cannot leave a separate user's entitlements readable from `localStorage`.
242
253
 
254
+ > **Call it on a real sign-out only — never on your auth listener's "not signed in yet" boot callback.** `reset()` is not a no-op on an anonymous device: it deletes the `anonymousId` from every store (including the shared cross-subdomain cookie) and mints a new one. Resetting on boot re-anonymises the visitor on every page load, so the pre-signup journey they arrived with — true source, UTMs, what they read — is orphaned from the account they create moments later. Latch that you have actually seen a signed-in user before treating a `null` as a sign-out. See [Guard `reset()`](https://cross-deck.com/docs/identify-users/#guard-reset).
255
+
243
256
  ### `Crossdeck.flush()`
244
257
 
245
258
  Force-flush the in-memory event queue. Useful before page unload or when shutting down a script. (`flushEvents()` is kept as a deprecated alias.)