@cross-deck/web 0.4.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/CHANGELOG.md +35 -0
- package/README.md +27 -5
- package/dist/index.cjs +207 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.mjs +207 -24
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +207 -24
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +207 -24
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
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) {
|
|
@@ -81,7 +81,8 @@ var HttpClient = class {
|
|
|
81
81
|
response = await fetch(url, {
|
|
82
82
|
method,
|
|
83
83
|
headers,
|
|
84
|
-
body: bodyInit
|
|
84
|
+
body: bodyInit,
|
|
85
|
+
keepalive: options.keepalive === true
|
|
85
86
|
});
|
|
86
87
|
} catch (err) {
|
|
87
88
|
throw new CrossdeckError({
|
|
@@ -126,19 +127,25 @@ var HttpClient = class {
|
|
|
126
127
|
var KEY_ANON = "anon_id";
|
|
127
128
|
var KEY_CDCUST = "cdcust_id";
|
|
128
129
|
var IdentityStore = class {
|
|
129
|
-
constructor(
|
|
130
|
-
this.
|
|
130
|
+
constructor(primary, prefix, secondary) {
|
|
131
|
+
this.primary = primary;
|
|
131
132
|
this.prefix = prefix;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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;
|
|
136
140
|
this.state = {
|
|
137
|
-
anonymousId:
|
|
138
|
-
crossdeckCustomerId:
|
|
141
|
+
anonymousId: anon ?? this.mintAnonymousId(),
|
|
142
|
+
crossdeckCustomerId: cdcust
|
|
139
143
|
};
|
|
140
|
-
if (!
|
|
141
|
-
|
|
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);
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
/** Return the persisted anonymous device ID (always set). */
|
|
@@ -152,7 +159,7 @@ var IdentityStore = class {
|
|
|
152
159
|
/** Persist a newly-resolved Crossdeck customer ID. */
|
|
153
160
|
setCrossdeckCustomerId(value) {
|
|
154
161
|
this.state.crossdeckCustomerId = value;
|
|
155
|
-
this.
|
|
162
|
+
this.writeBoth(this.prefix + KEY_CDCUST, value);
|
|
156
163
|
}
|
|
157
164
|
/**
|
|
158
165
|
* Wipe persisted identity. Called by reset() — used when an end-user
|
|
@@ -160,13 +167,13 @@ var IdentityStore = class {
|
|
|
160
167
|
* pre-login session is a fresh customer in the identity graph.
|
|
161
168
|
*/
|
|
162
169
|
reset() {
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
170
|
+
this.deleteBoth(this.prefix + KEY_ANON);
|
|
171
|
+
this.deleteBoth(this.prefix + KEY_CDCUST);
|
|
165
172
|
this.state = {
|
|
166
173
|
anonymousId: this.mintAnonymousId(),
|
|
167
174
|
crossdeckCustomerId: null
|
|
168
175
|
};
|
|
169
|
-
this.
|
|
176
|
+
this.writeBoth(this.prefix + KEY_ANON, this.state.anonymousId);
|
|
170
177
|
}
|
|
171
178
|
/**
|
|
172
179
|
* Generate an anonymousId. Crockford-ish base32 timestamp + random
|
|
@@ -178,6 +185,30 @@ var IdentityStore = class {
|
|
|
178
185
|
const rand = randomChars(10);
|
|
179
186
|
return `anon_${ts}${rand}`;
|
|
180
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
|
+
}
|
|
181
212
|
};
|
|
182
213
|
function randomChars(count) {
|
|
183
214
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
@@ -306,8 +337,12 @@ var EventQueue = class {
|
|
|
306
337
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
307
338
|
* completes (success or failure). On failure, events stay in the
|
|
308
339
|
* buffer for the next flush attempt.
|
|
340
|
+
*
|
|
341
|
+
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
342
|
+
* browser keeps the request alive past page unload. Use this for
|
|
343
|
+
* terminal flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
309
344
|
*/
|
|
310
|
-
async flush() {
|
|
345
|
+
async flush(options = {}) {
|
|
311
346
|
if (this.buffer.length === 0) return null;
|
|
312
347
|
this.cancelTimerIfSet();
|
|
313
348
|
const batch = this.buffer.splice(0);
|
|
@@ -323,7 +358,8 @@ var EventQueue = class {
|
|
|
323
358
|
environment: env.environment,
|
|
324
359
|
sdk: env.sdk,
|
|
325
360
|
events: batch
|
|
326
|
-
}
|
|
361
|
+
},
|
|
362
|
+
keepalive: options.keepalive === true
|
|
327
363
|
});
|
|
328
364
|
this.lastFlushAt = Date.now();
|
|
329
365
|
this.lastError = null;
|
|
@@ -398,6 +434,59 @@ var MemoryStorage = class {
|
|
|
398
434
|
this.store.delete(key);
|
|
399
435
|
}
|
|
400
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
|
+
};
|
|
401
490
|
function detectDefaultStorage() {
|
|
402
491
|
try {
|
|
403
492
|
const ls = globalThis.localStorage;
|
|
@@ -411,6 +500,17 @@ function detectDefaultStorage() {
|
|
|
411
500
|
}
|
|
412
501
|
return new MemoryStorage();
|
|
413
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
|
+
}
|
|
414
514
|
|
|
415
515
|
// src/device-info.ts
|
|
416
516
|
function isBrowser() {
|
|
@@ -511,6 +611,14 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
511
611
|
deviceInfo: true
|
|
512
612
|
};
|
|
513
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
|
+
};
|
|
514
622
|
var AutoTracker = class {
|
|
515
623
|
constructor(cfg, track) {
|
|
516
624
|
this.cfg = cfg;
|
|
@@ -546,6 +654,18 @@ var AutoTracker = class {
|
|
|
546
654
|
get currentSessionId() {
|
|
547
655
|
return this.session?.sessionId ?? null;
|
|
548
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
|
+
}
|
|
549
669
|
// ---------- sessions ----------
|
|
550
670
|
installSessionTracking() {
|
|
551
671
|
this.session = this.startNewSession();
|
|
@@ -583,7 +703,8 @@ var AutoTracker = class {
|
|
|
583
703
|
sessionId: mintSessionId(),
|
|
584
704
|
startedAt: Date.now(),
|
|
585
705
|
hiddenAt: null,
|
|
586
|
-
endedSent: false
|
|
706
|
+
endedSent: false,
|
|
707
|
+
acquisition: captureAcquisition()
|
|
587
708
|
};
|
|
588
709
|
}
|
|
589
710
|
emitSessionStart() {
|
|
@@ -649,6 +770,26 @@ function mintSessionId() {
|
|
|
649
770
|
const ts = Date.now().toString(36);
|
|
650
771
|
return `sess_${ts}${randomChars(10)}`;
|
|
651
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
|
+
}
|
|
652
793
|
|
|
653
794
|
// src/debug.ts
|
|
654
795
|
var SENSITIVE_KEY_PATTERNS = [
|
|
@@ -753,7 +894,11 @@ var CrossdeckClient = class {
|
|
|
753
894
|
storagePrefix: options.storagePrefix ?? "crossdeck:",
|
|
754
895
|
autoHeartbeat: options.autoHeartbeat ?? true,
|
|
755
896
|
eventFlushBatchSize: options.eventFlushBatchSize ?? 20,
|
|
756
|
-
|
|
897
|
+
// 1500ms idle window. Short enough that an event queued on page
|
|
898
|
+
// load still flushes if the user leaves quickly (the keepalive
|
|
899
|
+
// pagehide handler picks up anything that doesn't); long enough
|
|
900
|
+
// that bursts of clicks coalesce into one network round-trip.
|
|
901
|
+
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 1500,
|
|
757
902
|
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
758
903
|
autoTrack,
|
|
759
904
|
appVersion: options.appVersion ?? null
|
|
@@ -766,7 +911,10 @@ var CrossdeckClient = class {
|
|
|
766
911
|
sdkVersion: opts.sdkVersion
|
|
767
912
|
});
|
|
768
913
|
const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
|
|
769
|
-
const
|
|
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);
|
|
770
918
|
const entitlements = new EntitlementCache();
|
|
771
919
|
const events = new EventQueue({
|
|
772
920
|
http,
|
|
@@ -795,7 +943,8 @@ var CrossdeckClient = class {
|
|
|
795
943
|
deviceInfo,
|
|
796
944
|
options: opts,
|
|
797
945
|
debug,
|
|
798
|
-
developerUserId: null
|
|
946
|
+
developerUserId: null,
|
|
947
|
+
uninstallUnloadFlush: null
|
|
799
948
|
};
|
|
800
949
|
debug.emit("sdk.configured", `Crossdeck connected to ${opts.appId} in ${opts.environment} mode.`, {
|
|
801
950
|
appId: opts.appId,
|
|
@@ -810,6 +959,9 @@ var CrossdeckClient = class {
|
|
|
810
959
|
this.state.autoTracker = tracker;
|
|
811
960
|
tracker.install();
|
|
812
961
|
}
|
|
962
|
+
this.state.uninstallUnloadFlush = installUnloadFlush(() => {
|
|
963
|
+
void this.flush({ keepalive: true }).catch(() => void 0);
|
|
964
|
+
});
|
|
813
965
|
if (opts.autoHeartbeat) {
|
|
814
966
|
void this.heartbeat().catch(() => void 0);
|
|
815
967
|
}
|
|
@@ -943,6 +1095,15 @@ var CrossdeckClient = class {
|
|
|
943
1095
|
const enriched = { ...s.deviceInfo };
|
|
944
1096
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
945
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
|
+
}
|
|
946
1107
|
if (properties) Object.assign(enriched, properties);
|
|
947
1108
|
const event = {
|
|
948
1109
|
eventId: this.mintEventId(),
|
|
@@ -956,11 +1117,16 @@ var CrossdeckClient = class {
|
|
|
956
1117
|
/**
|
|
957
1118
|
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
958
1119
|
*
|
|
1120
|
+
* Pass `{ keepalive: true }` from terminal handlers (pagehide /
|
|
1121
|
+
* visibilitychange→hidden / beforeunload). The browser keeps the
|
|
1122
|
+
* request alive after the page tears down, so the final batch
|
|
1123
|
+
* actually lands instead of being cancelled with the unload.
|
|
1124
|
+
*
|
|
959
1125
|
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
960
1126
|
*/
|
|
961
|
-
async flush() {
|
|
1127
|
+
async flush(options = {}) {
|
|
962
1128
|
const s = this.requireStarted();
|
|
963
|
-
await s.events.flush();
|
|
1129
|
+
await s.events.flush(options);
|
|
964
1130
|
}
|
|
965
1131
|
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
966
1132
|
async flushEvents() {
|
|
@@ -1143,6 +1309,23 @@ function resolveAutoTrack(input) {
|
|
|
1143
1309
|
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo
|
|
1144
1310
|
};
|
|
1145
1311
|
}
|
|
1312
|
+
function installUnloadFlush(onUnload) {
|
|
1313
|
+
const w = globalThis.window;
|
|
1314
|
+
const doc = globalThis.document;
|
|
1315
|
+
if (!w || !doc) return () => void 0;
|
|
1316
|
+
const onVisChange = () => {
|
|
1317
|
+
if (doc.visibilityState === "hidden") onUnload();
|
|
1318
|
+
};
|
|
1319
|
+
const onTerminal = () => onUnload();
|
|
1320
|
+
doc.addEventListener("visibilitychange", onVisChange);
|
|
1321
|
+
w.addEventListener("pagehide", onTerminal);
|
|
1322
|
+
w.addEventListener("beforeunload", onTerminal);
|
|
1323
|
+
return () => {
|
|
1324
|
+
doc.removeEventListener("visibilitychange", onVisChange);
|
|
1325
|
+
w.removeEventListener("pagehide", onTerminal);
|
|
1326
|
+
w.removeEventListener("beforeunload", onTerminal);
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1146
1329
|
|
|
1147
1330
|
// src/react.ts
|
|
1148
1331
|
function useEntitlement(key) {
|