@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/index.mjs CHANGED
@@ -46,7 +46,7 @@ function typeMapForStatus(status) {
46
46
 
47
47
  // src/http.ts
48
48
  var SDK_NAME = "@cross-deck/web";
49
- var SDK_VERSION = "0.5.0";
49
+ var SDK_VERSION = "0.6.0";
50
50
  var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
51
51
  var HttpClient = class {
52
52
  constructor(config) {
@@ -124,19 +124,25 @@ var HttpClient = class {
124
124
  var KEY_ANON = "anon_id";
125
125
  var KEY_CDCUST = "cdcust_id";
126
126
  var IdentityStore = class {
127
- constructor(storage, prefix) {
128
- this.storage = storage;
127
+ constructor(primary, prefix, secondary) {
128
+ this.primary = primary;
129
129
  this.prefix = prefix;
130
- const stored = {
131
- anon: storage.getItem(prefix + KEY_ANON),
132
- cdcust: storage.getItem(prefix + KEY_CDCUST)
133
- };
130
+ this.secondary = secondary ?? null;
131
+ const anonFromPrimary = primary.getItem(prefix + KEY_ANON);
132
+ const cdcustFromPrimary = primary.getItem(prefix + KEY_CDCUST);
133
+ const anonFromSecondary = this.secondary?.getItem(prefix + KEY_ANON) ?? null;
134
+ const cdcustFromSecondary = this.secondary?.getItem(prefix + KEY_CDCUST) ?? null;
135
+ const anon = anonFromPrimary ?? anonFromSecondary;
136
+ const cdcust = cdcustFromPrimary ?? cdcustFromSecondary;
134
137
  this.state = {
135
- anonymousId: stored.anon ?? this.mintAnonymousId(),
136
- crossdeckCustomerId: stored.cdcust
138
+ anonymousId: anon ?? this.mintAnonymousId(),
139
+ crossdeckCustomerId: cdcust
137
140
  };
138
- if (!stored.anon) {
139
- storage.setItem(prefix + KEY_ANON, this.state.anonymousId);
141
+ if (!anonFromPrimary || !anonFromSecondary) {
142
+ this.writeBoth(prefix + KEY_ANON, this.state.anonymousId);
143
+ }
144
+ if (cdcust && (!cdcustFromPrimary || !cdcustFromSecondary)) {
145
+ this.writeBoth(prefix + KEY_CDCUST, cdcust);
140
146
  }
141
147
  }
142
148
  /** Return the persisted anonymous device ID (always set). */
@@ -150,7 +156,7 @@ var IdentityStore = class {
150
156
  /** Persist a newly-resolved Crossdeck customer ID. */
151
157
  setCrossdeckCustomerId(value) {
152
158
  this.state.crossdeckCustomerId = value;
153
- this.storage.setItem(this.prefix + KEY_CDCUST, value);
159
+ this.writeBoth(this.prefix + KEY_CDCUST, value);
154
160
  }
155
161
  /**
156
162
  * Wipe persisted identity. Called by reset() — used when an end-user
@@ -158,13 +164,13 @@ var IdentityStore = class {
158
164
  * pre-login session is a fresh customer in the identity graph.
159
165
  */
160
166
  reset() {
161
- this.storage.removeItem(this.prefix + KEY_ANON);
162
- this.storage.removeItem(this.prefix + KEY_CDCUST);
167
+ this.deleteBoth(this.prefix + KEY_ANON);
168
+ this.deleteBoth(this.prefix + KEY_CDCUST);
163
169
  this.state = {
164
170
  anonymousId: this.mintAnonymousId(),
165
171
  crossdeckCustomerId: null
166
172
  };
167
- this.storage.setItem(this.prefix + KEY_ANON, this.state.anonymousId);
173
+ this.writeBoth(this.prefix + KEY_ANON, this.state.anonymousId);
168
174
  }
169
175
  /**
170
176
  * Generate an anonymousId. Crockford-ish base32 timestamp + random
@@ -176,6 +182,30 @@ var IdentityStore = class {
176
182
  const rand = randomChars(10);
177
183
  return `anon_${ts}${rand}`;
178
184
  }
185
+ writeBoth(key, value) {
186
+ try {
187
+ this.primary.setItem(key, value);
188
+ } catch {
189
+ }
190
+ if (this.secondary) {
191
+ try {
192
+ this.secondary.setItem(key, value);
193
+ } catch {
194
+ }
195
+ }
196
+ }
197
+ deleteBoth(key) {
198
+ try {
199
+ this.primary.removeItem(key);
200
+ } catch {
201
+ }
202
+ if (this.secondary) {
203
+ try {
204
+ this.secondary.removeItem(key);
205
+ } catch {
206
+ }
207
+ }
208
+ }
179
209
  };
180
210
  function randomChars(count) {
181
211
  const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
@@ -401,6 +431,59 @@ var MemoryStorage = class {
401
431
  this.store.delete(key);
402
432
  }
403
433
  };
434
+ var CookieStorage = class {
435
+ constructor(options) {
436
+ this.maxAgeSec = options?.maxAgeSec ?? 63072e3;
437
+ this.secure = options?.secure ?? defaultSecure();
438
+ this.sameSite = options?.sameSite ?? "Lax";
439
+ }
440
+ getItem(key) {
441
+ if (!hasDocument()) return null;
442
+ const doc = globalThis.document;
443
+ const cookies = doc.cookie ? doc.cookie.split(/;\s*/) : [];
444
+ const prefix = encodeURIComponent(key) + "=";
445
+ for (const c of cookies) {
446
+ if (c.startsWith(prefix)) {
447
+ try {
448
+ return decodeURIComponent(c.slice(prefix.length));
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+ }
454
+ return null;
455
+ }
456
+ setItem(key, value) {
457
+ if (!hasDocument()) return;
458
+ const doc = globalThis.document;
459
+ const parts = [
460
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
461
+ "Path=/",
462
+ `Max-Age=${this.maxAgeSec}`,
463
+ `SameSite=${this.sameSite}`
464
+ ];
465
+ if (this.secure) parts.push("Secure");
466
+ try {
467
+ doc.cookie = parts.join("; ");
468
+ } catch {
469
+ }
470
+ }
471
+ removeItem(key) {
472
+ if (!hasDocument()) return;
473
+ const doc = globalThis.document;
474
+ const parts = [
475
+ `${encodeURIComponent(key)}=`,
476
+ "Path=/",
477
+ "Max-Age=0",
478
+ `SameSite=${this.sameSite}`
479
+ ];
480
+ if (this.secure) parts.push("Secure");
481
+ try {
482
+ doc.cookie = parts.join("; ");
483
+ } catch {
484
+ }
485
+ }
486
+ };
404
487
  function detectDefaultStorage() {
405
488
  try {
406
489
  const ls = globalThis.localStorage;
@@ -414,6 +497,17 @@ function detectDefaultStorage() {
414
497
  }
415
498
  return new MemoryStorage();
416
499
  }
500
+ function defaultSecure() {
501
+ try {
502
+ const loc = globalThis.location;
503
+ return loc?.protocol === "https:";
504
+ } catch {
505
+ return false;
506
+ }
507
+ }
508
+ function hasDocument() {
509
+ return typeof globalThis.document !== "undefined";
510
+ }
417
511
 
418
512
  // src/device-info.ts
419
513
  function isBrowser() {
@@ -514,6 +608,14 @@ var DEFAULT_AUTO_TRACK = {
514
608
  deviceInfo: true
515
609
  };
516
610
  var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
611
+ var EMPTY_ACQUISITION = {
612
+ utm_source: "",
613
+ utm_medium: "",
614
+ utm_campaign: "",
615
+ utm_content: "",
616
+ utm_term: "",
617
+ referrer: ""
618
+ };
517
619
  var AutoTracker = class {
518
620
  constructor(cfg, track) {
519
621
  this.cfg = cfg;
@@ -549,6 +651,18 @@ var AutoTracker = class {
549
651
  get currentSessionId() {
550
652
  return this.session?.sessionId ?? null;
551
653
  }
654
+ /**
655
+ * Per-session acquisition context — utm_* + referrer, captured once
656
+ * at session start. Returns empty strings when there's no session
657
+ * (Node, before init, after uninstall) so callers can spread without
658
+ * conditional logic. Bank-grade rule: capture once, attach to every
659
+ * event of the session, don't re-read on every track() (the URL
660
+ * changes via SPA pushState; the source-of-record is the URL we
661
+ * landed on).
662
+ */
663
+ get currentAcquisition() {
664
+ return this.session?.acquisition ?? EMPTY_ACQUISITION;
665
+ }
552
666
  // ---------- sessions ----------
553
667
  installSessionTracking() {
554
668
  this.session = this.startNewSession();
@@ -586,7 +700,8 @@ var AutoTracker = class {
586
700
  sessionId: mintSessionId(),
587
701
  startedAt: Date.now(),
588
702
  hiddenAt: null,
589
- endedSent: false
703
+ endedSent: false,
704
+ acquisition: captureAcquisition()
590
705
  };
591
706
  }
592
707
  emitSessionStart() {
@@ -652,6 +767,26 @@ function mintSessionId() {
652
767
  const ts = Date.now().toString(36);
653
768
  return `sess_${ts}${randomChars(10)}`;
654
769
  }
770
+ function captureAcquisition() {
771
+ if (!isBrowserSafe()) return { ...EMPTY_ACQUISITION };
772
+ const result = { ...EMPTY_ACQUISITION };
773
+ try {
774
+ const w = globalThis.window;
775
+ const params = new URLSearchParams(w.location.search ?? "");
776
+ result.utm_source = params.get("utm_source") ?? "";
777
+ result.utm_medium = params.get("utm_medium") ?? "";
778
+ result.utm_campaign = params.get("utm_campaign") ?? "";
779
+ result.utm_content = params.get("utm_content") ?? "";
780
+ result.utm_term = params.get("utm_term") ?? "";
781
+ } catch {
782
+ }
783
+ try {
784
+ const doc = globalThis.document;
785
+ if (typeof doc.referrer === "string") result.referrer = doc.referrer;
786
+ } catch {
787
+ }
788
+ return result;
789
+ }
655
790
 
656
791
  // src/debug.ts
657
792
  var SENSITIVE_KEY_PATTERNS = [
@@ -773,7 +908,10 @@ var CrossdeckClient = class {
773
908
  sdkVersion: opts.sdkVersion
774
909
  });
775
910
  const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
776
- const identity = new IdentityStore(effectiveStorage, opts.storagePrefix);
911
+ const useCookieRedundancy = persistIdentity && !options.storage && // honour caller's adapter choice
912
+ typeof globalThis.document !== "undefined";
913
+ const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
914
+ const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
777
915
  const entitlements = new EntitlementCache();
778
916
  const events = new EventQueue({
779
917
  http,
@@ -954,6 +1092,15 @@ var CrossdeckClient = class {
954
1092
  const enriched = { ...s.deviceInfo };
955
1093
  const sessionId = s.autoTracker?.currentSessionId;
956
1094
  if (sessionId) enriched.sessionId = sessionId;
1095
+ const acquisition = s.autoTracker?.currentAcquisition;
1096
+ if (acquisition) {
1097
+ if (acquisition.utm_source) enriched.utm_source = acquisition.utm_source;
1098
+ if (acquisition.utm_medium) enriched.utm_medium = acquisition.utm_medium;
1099
+ if (acquisition.utm_campaign) enriched.utm_campaign = acquisition.utm_campaign;
1100
+ if (acquisition.utm_content) enriched.utm_content = acquisition.utm_content;
1101
+ if (acquisition.utm_term) enriched.utm_term = acquisition.utm_term;
1102
+ if (acquisition.referrer) enriched.referrer = acquisition.referrer;
1103
+ }
957
1104
  if (properties) Object.assign(enriched, properties);
958
1105
  const event = {
959
1106
  eventId: this.mintEventId(),