@cross-deck/web 0.5.0 → 0.6.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.
package/dist/react.cjs CHANGED
@@ -74,7 +74,7 @@ function typeMapForStatus(status) {
74
74
 
75
75
  // src/http.ts
76
76
  var SDK_NAME = "@cross-deck/web";
77
- var SDK_VERSION = "0.5.0";
77
+ var SDK_VERSION = "0.6.0";
78
78
  var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
79
79
  var HttpClient = class {
80
80
  constructor(config) {
@@ -152,19 +152,25 @@ var HttpClient = class {
152
152
  var KEY_ANON = "anon_id";
153
153
  var KEY_CDCUST = "cdcust_id";
154
154
  var IdentityStore = class {
155
- constructor(storage, prefix) {
156
- this.storage = storage;
155
+ constructor(primary, prefix, secondary) {
156
+ this.primary = primary;
157
157
  this.prefix = prefix;
158
- const stored = {
159
- anon: storage.getItem(prefix + KEY_ANON),
160
- cdcust: storage.getItem(prefix + KEY_CDCUST)
161
- };
158
+ this.secondary = secondary ?? null;
159
+ const anonFromPrimary = primary.getItem(prefix + KEY_ANON);
160
+ const cdcustFromPrimary = primary.getItem(prefix + KEY_CDCUST);
161
+ const anonFromSecondary = this.secondary?.getItem(prefix + KEY_ANON) ?? null;
162
+ const cdcustFromSecondary = this.secondary?.getItem(prefix + KEY_CDCUST) ?? null;
163
+ const anon = anonFromPrimary ?? anonFromSecondary;
164
+ const cdcust = cdcustFromPrimary ?? cdcustFromSecondary;
162
165
  this.state = {
163
- anonymousId: stored.anon ?? this.mintAnonymousId(),
164
- crossdeckCustomerId: stored.cdcust
166
+ anonymousId: anon ?? this.mintAnonymousId(),
167
+ crossdeckCustomerId: cdcust
165
168
  };
166
- if (!stored.anon) {
167
- storage.setItem(prefix + KEY_ANON, this.state.anonymousId);
169
+ if (!anonFromPrimary || !anonFromSecondary) {
170
+ this.writeBoth(prefix + KEY_ANON, this.state.anonymousId);
171
+ }
172
+ if (cdcust && (!cdcustFromPrimary || !cdcustFromSecondary)) {
173
+ this.writeBoth(prefix + KEY_CDCUST, cdcust);
168
174
  }
169
175
  }
170
176
  /** Return the persisted anonymous device ID (always set). */
@@ -178,7 +184,7 @@ var IdentityStore = class {
178
184
  /** Persist a newly-resolved Crossdeck customer ID. */
179
185
  setCrossdeckCustomerId(value) {
180
186
  this.state.crossdeckCustomerId = value;
181
- this.storage.setItem(this.prefix + KEY_CDCUST, value);
187
+ this.writeBoth(this.prefix + KEY_CDCUST, value);
182
188
  }
183
189
  /**
184
190
  * Wipe persisted identity. Called by reset() — used when an end-user
@@ -186,13 +192,13 @@ var IdentityStore = class {
186
192
  * pre-login session is a fresh customer in the identity graph.
187
193
  */
188
194
  reset() {
189
- this.storage.removeItem(this.prefix + KEY_ANON);
190
- this.storage.removeItem(this.prefix + KEY_CDCUST);
195
+ this.deleteBoth(this.prefix + KEY_ANON);
196
+ this.deleteBoth(this.prefix + KEY_CDCUST);
191
197
  this.state = {
192
198
  anonymousId: this.mintAnonymousId(),
193
199
  crossdeckCustomerId: null
194
200
  };
195
- this.storage.setItem(this.prefix + KEY_ANON, this.state.anonymousId);
201
+ this.writeBoth(this.prefix + KEY_ANON, this.state.anonymousId);
196
202
  }
197
203
  /**
198
204
  * Generate an anonymousId. Crockford-ish base32 timestamp + random
@@ -204,6 +210,30 @@ var IdentityStore = class {
204
210
  const rand = randomChars(10);
205
211
  return `anon_${ts}${rand}`;
206
212
  }
213
+ writeBoth(key, value) {
214
+ try {
215
+ this.primary.setItem(key, value);
216
+ } catch {
217
+ }
218
+ if (this.secondary) {
219
+ try {
220
+ this.secondary.setItem(key, value);
221
+ } catch {
222
+ }
223
+ }
224
+ }
225
+ deleteBoth(key) {
226
+ try {
227
+ this.primary.removeItem(key);
228
+ } catch {
229
+ }
230
+ if (this.secondary) {
231
+ try {
232
+ this.secondary.removeItem(key);
233
+ } catch {
234
+ }
235
+ }
236
+ }
207
237
  };
208
238
  function randomChars(count) {
209
239
  const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
@@ -429,6 +459,59 @@ var MemoryStorage = class {
429
459
  this.store.delete(key);
430
460
  }
431
461
  };
462
+ var CookieStorage = class {
463
+ constructor(options) {
464
+ this.maxAgeSec = options?.maxAgeSec ?? 63072e3;
465
+ this.secure = options?.secure ?? defaultSecure();
466
+ this.sameSite = options?.sameSite ?? "Lax";
467
+ }
468
+ getItem(key) {
469
+ if (!hasDocument()) return null;
470
+ const doc = globalThis.document;
471
+ const cookies = doc.cookie ? doc.cookie.split(/;\s*/) : [];
472
+ const prefix = encodeURIComponent(key) + "=";
473
+ for (const c of cookies) {
474
+ if (c.startsWith(prefix)) {
475
+ try {
476
+ return decodeURIComponent(c.slice(prefix.length));
477
+ } catch {
478
+ return null;
479
+ }
480
+ }
481
+ }
482
+ return null;
483
+ }
484
+ setItem(key, value) {
485
+ if (!hasDocument()) return;
486
+ const doc = globalThis.document;
487
+ const parts = [
488
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
489
+ "Path=/",
490
+ `Max-Age=${this.maxAgeSec}`,
491
+ `SameSite=${this.sameSite}`
492
+ ];
493
+ if (this.secure) parts.push("Secure");
494
+ try {
495
+ doc.cookie = parts.join("; ");
496
+ } catch {
497
+ }
498
+ }
499
+ removeItem(key) {
500
+ if (!hasDocument()) return;
501
+ const doc = globalThis.document;
502
+ const parts = [
503
+ `${encodeURIComponent(key)}=`,
504
+ "Path=/",
505
+ "Max-Age=0",
506
+ `SameSite=${this.sameSite}`
507
+ ];
508
+ if (this.secure) parts.push("Secure");
509
+ try {
510
+ doc.cookie = parts.join("; ");
511
+ } catch {
512
+ }
513
+ }
514
+ };
432
515
  function detectDefaultStorage() {
433
516
  try {
434
517
  const ls = globalThis.localStorage;
@@ -442,6 +525,17 @@ function detectDefaultStorage() {
442
525
  }
443
526
  return new MemoryStorage();
444
527
  }
528
+ function defaultSecure() {
529
+ try {
530
+ const loc = globalThis.location;
531
+ return loc?.protocol === "https:";
532
+ } catch {
533
+ return false;
534
+ }
535
+ }
536
+ function hasDocument() {
537
+ return typeof globalThis.document !== "undefined";
538
+ }
445
539
 
446
540
  // src/device-info.ts
447
541
  function isBrowser() {
@@ -542,6 +636,14 @@ var DEFAULT_AUTO_TRACK = {
542
636
  deviceInfo: true
543
637
  };
544
638
  var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
639
+ var EMPTY_ACQUISITION = {
640
+ utm_source: "",
641
+ utm_medium: "",
642
+ utm_campaign: "",
643
+ utm_content: "",
644
+ utm_term: "",
645
+ referrer: ""
646
+ };
545
647
  var AutoTracker = class {
546
648
  constructor(cfg, track) {
547
649
  this.cfg = cfg;
@@ -577,6 +679,18 @@ var AutoTracker = class {
577
679
  get currentSessionId() {
578
680
  return this.session?.sessionId ?? null;
579
681
  }
682
+ /**
683
+ * Per-session acquisition context — utm_* + referrer, captured once
684
+ * at session start. Returns empty strings when there's no session
685
+ * (Node, before init, after uninstall) so callers can spread without
686
+ * conditional logic. Bank-grade rule: capture once, attach to every
687
+ * event of the session, don't re-read on every track() (the URL
688
+ * changes via SPA pushState; the source-of-record is the URL we
689
+ * landed on).
690
+ */
691
+ get currentAcquisition() {
692
+ return this.session?.acquisition ?? EMPTY_ACQUISITION;
693
+ }
580
694
  // ---------- sessions ----------
581
695
  installSessionTracking() {
582
696
  this.session = this.startNewSession();
@@ -614,7 +728,8 @@ var AutoTracker = class {
614
728
  sessionId: mintSessionId(),
615
729
  startedAt: Date.now(),
616
730
  hiddenAt: null,
617
- endedSent: false
731
+ endedSent: false,
732
+ acquisition: captureAcquisition()
618
733
  };
619
734
  }
620
735
  emitSessionStart() {
@@ -680,6 +795,26 @@ function mintSessionId() {
680
795
  const ts = Date.now().toString(36);
681
796
  return `sess_${ts}${randomChars(10)}`;
682
797
  }
798
+ function captureAcquisition() {
799
+ if (!isBrowserSafe()) return { ...EMPTY_ACQUISITION };
800
+ const result = { ...EMPTY_ACQUISITION };
801
+ try {
802
+ const w = globalThis.window;
803
+ const params = new URLSearchParams(w.location.search ?? "");
804
+ result.utm_source = params.get("utm_source") ?? "";
805
+ result.utm_medium = params.get("utm_medium") ?? "";
806
+ result.utm_campaign = params.get("utm_campaign") ?? "";
807
+ result.utm_content = params.get("utm_content") ?? "";
808
+ result.utm_term = params.get("utm_term") ?? "";
809
+ } catch {
810
+ }
811
+ try {
812
+ const doc = globalThis.document;
813
+ if (typeof doc.referrer === "string") result.referrer = doc.referrer;
814
+ } catch {
815
+ }
816
+ return result;
817
+ }
683
818
 
684
819
  // src/debug.ts
685
820
  var SENSITIVE_KEY_PATTERNS = [
@@ -801,7 +936,10 @@ var CrossdeckClient = class {
801
936
  sdkVersion: opts.sdkVersion
802
937
  });
803
938
  const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
804
- const identity = new IdentityStore(effectiveStorage, opts.storagePrefix);
939
+ const useCookieRedundancy = persistIdentity && !options.storage && // honour caller's adapter choice
940
+ typeof globalThis.document !== "undefined";
941
+ const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
942
+ const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
805
943
  const entitlements = new EntitlementCache();
806
944
  const events = new EventQueue({
807
945
  http,
@@ -982,6 +1120,15 @@ var CrossdeckClient = class {
982
1120
  const enriched = { ...s.deviceInfo };
983
1121
  const sessionId = s.autoTracker?.currentSessionId;
984
1122
  if (sessionId) enriched.sessionId = sessionId;
1123
+ const acquisition = s.autoTracker?.currentAcquisition;
1124
+ if (acquisition) {
1125
+ if (acquisition.utm_source) enriched.utm_source = acquisition.utm_source;
1126
+ if (acquisition.utm_medium) enriched.utm_medium = acquisition.utm_medium;
1127
+ if (acquisition.utm_campaign) enriched.utm_campaign = acquisition.utm_campaign;
1128
+ if (acquisition.utm_content) enriched.utm_content = acquisition.utm_content;
1129
+ if (acquisition.utm_term) enriched.utm_term = acquisition.utm_term;
1130
+ if (acquisition.referrer) enriched.referrer = acquisition.referrer;
1131
+ }
985
1132
  if (properties) Object.assign(enriched, properties);
986
1133
  const event = {
987
1134
  eventId: this.mintEventId(),