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