@cross-deck/web 0.1.1 → 0.3.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 +70 -16
- package/dist/index.d.mts +145 -12
- package/dist/index.d.ts +145 -12
- package/dist/index.js +479 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +479 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ function typeMapForStatus(status) {
|
|
|
78
78
|
|
|
79
79
|
// src/http.ts
|
|
80
80
|
var SDK_NAME = "@cross-deck/web";
|
|
81
|
-
var SDK_VERSION = "0.
|
|
81
|
+
var SDK_VERSION = "0.3.0";
|
|
82
82
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
83
83
|
var HttpClient = class {
|
|
84
84
|
constructor(config) {
|
|
@@ -277,6 +277,7 @@ var EventQueue = class {
|
|
|
277
277
|
this.lastFlushAt = 0;
|
|
278
278
|
this.lastError = null;
|
|
279
279
|
this.cancelTimer = null;
|
|
280
|
+
this.firstFlushFired = false;
|
|
280
281
|
}
|
|
281
282
|
enqueue(event) {
|
|
282
283
|
this.buffer.push(event);
|
|
@@ -303,12 +304,25 @@ var EventQueue = class {
|
|
|
303
304
|
const batch = this.buffer.splice(0);
|
|
304
305
|
this.inFlight += batch.length;
|
|
305
306
|
try {
|
|
307
|
+
const env = this.cfg.envelope();
|
|
306
308
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
307
|
-
body: {
|
|
309
|
+
body: {
|
|
310
|
+
// NorthStar §13.1 batch envelope. The backend validates these
|
|
311
|
+
// against the API-key-resolved app and rejects mismatches loudly
|
|
312
|
+
// (env_mismatch).
|
|
313
|
+
appId: env.appId,
|
|
314
|
+
environment: env.environment,
|
|
315
|
+
sdk: env.sdk,
|
|
316
|
+
events: batch
|
|
317
|
+
}
|
|
308
318
|
});
|
|
309
319
|
this.lastFlushAt = Date.now();
|
|
310
320
|
this.lastError = null;
|
|
311
321
|
this.inFlight -= batch.length;
|
|
322
|
+
if (!this.firstFlushFired) {
|
|
323
|
+
this.firstFlushFired = true;
|
|
324
|
+
this.cfg.onFirstFlushSuccess?.();
|
|
325
|
+
}
|
|
312
326
|
return result;
|
|
313
327
|
} catch (err) {
|
|
314
328
|
this.buffer.unshift(...batch);
|
|
@@ -389,36 +403,354 @@ function detectDefaultStorage() {
|
|
|
389
403
|
return new MemoryStorage();
|
|
390
404
|
}
|
|
391
405
|
|
|
406
|
+
// src/device-info.ts
|
|
407
|
+
function isBrowser() {
|
|
408
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined" && typeof globalThis.navigator !== "undefined";
|
|
409
|
+
}
|
|
410
|
+
function collectDeviceInfo(extra) {
|
|
411
|
+
const info = {};
|
|
412
|
+
if (extra?.appVersion) info.appVersion = extra.appVersion;
|
|
413
|
+
if (!isBrowser()) return info;
|
|
414
|
+
const w = globalThis.window;
|
|
415
|
+
const nav = globalThis.navigator;
|
|
416
|
+
const doc = globalThis.document;
|
|
417
|
+
try {
|
|
418
|
+
if (typeof nav.language === "string") info.locale = nav.language;
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
if (w.screen) {
|
|
427
|
+
info.screenWidth = w.screen.width;
|
|
428
|
+
info.screenHeight = w.screen.height;
|
|
429
|
+
}
|
|
430
|
+
info.viewportWidth = w.innerWidth;
|
|
431
|
+
info.viewportHeight = w.innerHeight;
|
|
432
|
+
info.devicePixelRatio = w.devicePixelRatio;
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const ua = nav.userAgent ?? "";
|
|
437
|
+
const parsed = parseUserAgent(ua);
|
|
438
|
+
Object.assign(info, parsed);
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const uaData = nav.userAgentData;
|
|
443
|
+
if (uaData?.platform && !info.os) info.os = uaData.platform;
|
|
444
|
+
if (uaData?.brands && !info.browser) {
|
|
445
|
+
const real = uaData.brands.find(
|
|
446
|
+
(b) => !/Not[ .;A]*Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
|
|
447
|
+
);
|
|
448
|
+
if (real) {
|
|
449
|
+
info.browser = real.brand;
|
|
450
|
+
info.browserVersion = real.version;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
void doc;
|
|
456
|
+
return info;
|
|
457
|
+
}
|
|
458
|
+
function parseUserAgent(ua) {
|
|
459
|
+
const out = {};
|
|
460
|
+
if (/iPad|iPhone|iPod/.test(ua)) {
|
|
461
|
+
out.os = "iOS";
|
|
462
|
+
const m = ua.match(/OS (\d+[._]\d+(?:[._]\d+)?)/);
|
|
463
|
+
if (m?.[1]) out.osVersion = m[1].replace(/_/g, ".");
|
|
464
|
+
} else if (/Android/.test(ua)) {
|
|
465
|
+
out.os = "Android";
|
|
466
|
+
const m = ua.match(/Android (\d+(?:\.\d+)*)/);
|
|
467
|
+
if (m?.[1]) out.osVersion = m[1];
|
|
468
|
+
} else if (/Windows/.test(ua)) {
|
|
469
|
+
out.os = "Windows";
|
|
470
|
+
const m = ua.match(/Windows NT (\d+\.\d+)/);
|
|
471
|
+
if (m?.[1]) out.osVersion = m[1];
|
|
472
|
+
} else if (/Mac OS X|Macintosh/.test(ua)) {
|
|
473
|
+
out.os = "macOS";
|
|
474
|
+
const m = ua.match(/Mac OS X (\d+[._]\d+(?:[._]\d+)?)/);
|
|
475
|
+
if (m?.[1]) out.osVersion = m[1].replace(/_/g, ".");
|
|
476
|
+
} else if (/Linux/.test(ua)) {
|
|
477
|
+
out.os = "Linux";
|
|
478
|
+
}
|
|
479
|
+
if (/Edg\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
480
|
+
out.browser = "Edge";
|
|
481
|
+
out.browserVersion = ua.match(/Edg\/(\d+(?:\.\d+)*)/)?.[1];
|
|
482
|
+
} else if (/Firefox\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
483
|
+
out.browser = "Firefox";
|
|
484
|
+
out.browserVersion = ua.match(/Firefox\/(\d+(?:\.\d+)*)/)?.[1];
|
|
485
|
+
} else if (/OPR\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
486
|
+
out.browser = "Opera";
|
|
487
|
+
out.browserVersion = ua.match(/OPR\/(\d+(?:\.\d+)*)/)?.[1];
|
|
488
|
+
} else if (/Chrome\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
489
|
+
out.browser = "Chrome";
|
|
490
|
+
out.browserVersion = ua.match(/Chrome\/(\d+(?:\.\d+)*)/)?.[1];
|
|
491
|
+
} else if (/Version\/(\d+(?:\.\d+)*).*Safari/.test(ua)) {
|
|
492
|
+
out.browser = "Safari";
|
|
493
|
+
out.browserVersion = ua.match(/Version\/(\d+(?:\.\d+)*)/)?.[1];
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/auto-track.ts
|
|
499
|
+
var DEFAULT_AUTO_TRACK = {
|
|
500
|
+
sessions: true,
|
|
501
|
+
pageViews: true,
|
|
502
|
+
deviceInfo: true
|
|
503
|
+
};
|
|
504
|
+
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
505
|
+
var AutoTracker = class {
|
|
506
|
+
constructor(cfg, track) {
|
|
507
|
+
this.cfg = cfg;
|
|
508
|
+
this.track = track;
|
|
509
|
+
this.session = null;
|
|
510
|
+
this.cleanups = [];
|
|
511
|
+
}
|
|
512
|
+
install() {
|
|
513
|
+
if (!isBrowserSafe()) return;
|
|
514
|
+
if (this.cfg.sessions) this.installSessionTracking();
|
|
515
|
+
if (this.cfg.pageViews) this.installPageViewTracking();
|
|
516
|
+
}
|
|
517
|
+
uninstall() {
|
|
518
|
+
while (this.cleanups.length) {
|
|
519
|
+
const fn = this.cleanups.pop();
|
|
520
|
+
try {
|
|
521
|
+
fn?.();
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (this.session && !this.session.endedSent) {
|
|
526
|
+
this.emitSessionEnd();
|
|
527
|
+
}
|
|
528
|
+
this.session = null;
|
|
529
|
+
}
|
|
530
|
+
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
531
|
+
resetSession() {
|
|
532
|
+
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
533
|
+
this.session = this.startNewSession();
|
|
534
|
+
this.emitSessionStart();
|
|
535
|
+
}
|
|
536
|
+
/** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
|
|
537
|
+
get currentSessionId() {
|
|
538
|
+
return this.session?.sessionId ?? null;
|
|
539
|
+
}
|
|
540
|
+
// ---------- sessions ----------
|
|
541
|
+
installSessionTracking() {
|
|
542
|
+
this.session = this.startNewSession();
|
|
543
|
+
this.emitSessionStart();
|
|
544
|
+
const onVisChange = () => {
|
|
545
|
+
if (!this.session) return;
|
|
546
|
+
const doc2 = globalThis.document;
|
|
547
|
+
if (doc2.visibilityState === "hidden") {
|
|
548
|
+
this.session.hiddenAt = Date.now();
|
|
549
|
+
} else if (doc2.visibilityState === "visible") {
|
|
550
|
+
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
551
|
+
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
552
|
+
this.emitSessionEnd();
|
|
553
|
+
this.session = this.startNewSession();
|
|
554
|
+
this.emitSessionStart();
|
|
555
|
+
} else {
|
|
556
|
+
this.session.hiddenAt = null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const onPageHide = () => this.emitSessionEnd();
|
|
561
|
+
const w = globalThis.window;
|
|
562
|
+
const doc = globalThis.document;
|
|
563
|
+
doc.addEventListener("visibilitychange", onVisChange);
|
|
564
|
+
w.addEventListener("pagehide", onPageHide);
|
|
565
|
+
w.addEventListener("beforeunload", onPageHide);
|
|
566
|
+
this.cleanups.push(() => {
|
|
567
|
+
doc.removeEventListener("visibilitychange", onVisChange);
|
|
568
|
+
w.removeEventListener("pagehide", onPageHide);
|
|
569
|
+
w.removeEventListener("beforeunload", onPageHide);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
startNewSession() {
|
|
573
|
+
return {
|
|
574
|
+
sessionId: mintSessionId(),
|
|
575
|
+
startedAt: Date.now(),
|
|
576
|
+
hiddenAt: null,
|
|
577
|
+
endedSent: false
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
emitSessionStart() {
|
|
581
|
+
if (!this.session) return;
|
|
582
|
+
this.track("session.started", { sessionId: this.session.sessionId });
|
|
583
|
+
}
|
|
584
|
+
emitSessionEnd() {
|
|
585
|
+
if (!this.session || this.session.endedSent) return;
|
|
586
|
+
const duration = Date.now() - this.session.startedAt;
|
|
587
|
+
this.track("session.ended", {
|
|
588
|
+
sessionId: this.session.sessionId,
|
|
589
|
+
durationMs: duration
|
|
590
|
+
});
|
|
591
|
+
this.session.endedSent = true;
|
|
592
|
+
}
|
|
593
|
+
// ---------- page views ----------
|
|
594
|
+
installPageViewTracking() {
|
|
595
|
+
const w = globalThis.window;
|
|
596
|
+
const doc = globalThis.document;
|
|
597
|
+
const fire = () => {
|
|
598
|
+
const loc = w.location;
|
|
599
|
+
this.track("page.viewed", {
|
|
600
|
+
path: loc.pathname,
|
|
601
|
+
url: loc.href,
|
|
602
|
+
search: loc.search || void 0,
|
|
603
|
+
hash: loc.hash || void 0,
|
|
604
|
+
title: doc.title,
|
|
605
|
+
// referrer only on the first hit of the session — afterward it's
|
|
606
|
+
// always our previous URL, which isn't useful.
|
|
607
|
+
referrer: doc.referrer || void 0
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
fire();
|
|
611
|
+
const origPush = w.history.pushState;
|
|
612
|
+
const origReplace = w.history.replaceState;
|
|
613
|
+
function patchedPush(data, unused, url) {
|
|
614
|
+
origPush.apply(this, [data, unused, url]);
|
|
615
|
+
queueMicrotask(fire);
|
|
616
|
+
}
|
|
617
|
+
function patchedReplace(data, unused, url) {
|
|
618
|
+
origReplace.apply(this, [data, unused, url]);
|
|
619
|
+
queueMicrotask(fire);
|
|
620
|
+
}
|
|
621
|
+
w.history.pushState = patchedPush;
|
|
622
|
+
w.history.replaceState = patchedReplace;
|
|
623
|
+
const onPopState = () => fire();
|
|
624
|
+
w.addEventListener("popstate", onPopState);
|
|
625
|
+
this.cleanups.push(() => {
|
|
626
|
+
if (w.history.pushState === patchedPush) {
|
|
627
|
+
w.history.pushState = origPush;
|
|
628
|
+
}
|
|
629
|
+
if (w.history.replaceState === patchedReplace) {
|
|
630
|
+
w.history.replaceState = origReplace;
|
|
631
|
+
}
|
|
632
|
+
w.removeEventListener("popstate", onPopState);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
function isBrowserSafe() {
|
|
637
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
638
|
+
}
|
|
639
|
+
function mintSessionId() {
|
|
640
|
+
const ts = Date.now().toString(36);
|
|
641
|
+
return `sess_${ts}${randomChars(10)}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/debug.ts
|
|
645
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
646
|
+
/^email$/i,
|
|
647
|
+
/^password$/i,
|
|
648
|
+
/^token$/i,
|
|
649
|
+
/^secret$/i,
|
|
650
|
+
/^card$/i,
|
|
651
|
+
/^phone$/i,
|
|
652
|
+
/password/i,
|
|
653
|
+
/credit_?card/i
|
|
654
|
+
];
|
|
655
|
+
function findSensitivePropertyKeys(properties) {
|
|
656
|
+
if (!properties) return [];
|
|
657
|
+
const hits = [];
|
|
658
|
+
for (const k of Object.keys(properties)) {
|
|
659
|
+
if (SENSITIVE_KEY_PATTERNS.some((re) => re.test(k))) hits.push(k);
|
|
660
|
+
}
|
|
661
|
+
return hits;
|
|
662
|
+
}
|
|
663
|
+
var ConsoleDebugLogger = class {
|
|
664
|
+
constructor() {
|
|
665
|
+
this.enabled = false;
|
|
666
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
667
|
+
}
|
|
668
|
+
emit(signal, message, context) {
|
|
669
|
+
if (!this.enabled) return;
|
|
670
|
+
if (ONCE_SIGNALS.has(signal)) {
|
|
671
|
+
if (this.seen.has(signal)) return;
|
|
672
|
+
this.seen.add(signal);
|
|
673
|
+
}
|
|
674
|
+
const ctx = context ? ` ${safeJson(context)}` : "";
|
|
675
|
+
console.info(`[crossdeck:${signal}] ${message}${ctx}`);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var ONCE_SIGNALS = /* @__PURE__ */ new Set([
|
|
679
|
+
"sdk.configured",
|
|
680
|
+
"sdk.first_event_sent",
|
|
681
|
+
"sdk.environment_mismatch"
|
|
682
|
+
]);
|
|
683
|
+
function safeJson(obj) {
|
|
684
|
+
try {
|
|
685
|
+
return JSON.stringify(obj);
|
|
686
|
+
} catch {
|
|
687
|
+
return "[unserialisable context]";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
392
691
|
// src/crossdeck.ts
|
|
393
692
|
var CrossdeckClient = class {
|
|
394
693
|
constructor() {
|
|
395
694
|
this.state = null;
|
|
396
695
|
}
|
|
397
696
|
/**
|
|
398
|
-
* Boot the SDK. Idempotent — calling
|
|
697
|
+
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
399
698
|
* is a no-op; calling with different options replaces the previous
|
|
400
699
|
* configuration.
|
|
700
|
+
*
|
|
701
|
+
* NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey,
|
|
702
|
+
* environment })`. The trio is validated up-front so a typo'd key or a
|
|
703
|
+
* mismatched env fails fast at boot rather than at first event-flush.
|
|
401
704
|
*/
|
|
402
|
-
|
|
705
|
+
init(options) {
|
|
403
706
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
404
707
|
throw new CrossdeckError({
|
|
405
708
|
type: "configuration_error",
|
|
406
709
|
code: "invalid_public_key",
|
|
407
|
-
message: "Crossdeck.
|
|
710
|
+
message: "Crossdeck.init requires a publishable key starting with cd_pub_."
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
if (!options.appId) {
|
|
714
|
+
throw new CrossdeckError({
|
|
715
|
+
type: "configuration_error",
|
|
716
|
+
code: "missing_app_id",
|
|
717
|
+
message: "Crossdeck.init requires an appId. Find yours in the Crossdeck dashboard."
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (options.environment !== "production" && options.environment !== "sandbox") {
|
|
721
|
+
throw new CrossdeckError({
|
|
722
|
+
type: "configuration_error",
|
|
723
|
+
code: "invalid_environment",
|
|
724
|
+
message: 'Crossdeck.init requires environment: "production" | "sandbox".'
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const keyEnv = inferEnvFromKey(options.publicKey);
|
|
728
|
+
if (keyEnv && keyEnv !== options.environment) {
|
|
729
|
+
throw new CrossdeckError({
|
|
730
|
+
type: "configuration_error",
|
|
731
|
+
code: "environment_mismatch",
|
|
732
|
+
message: `Crossdeck.init: environment "${options.environment}" disagrees with key prefix (${keyEnv}). Reconcile the publishable key with the environment declaration.`
|
|
408
733
|
});
|
|
409
734
|
}
|
|
410
735
|
const storage = options.storage ?? detectDefaultStorage();
|
|
411
736
|
const persistIdentity = options.persistIdentity ?? true;
|
|
737
|
+
const autoTrack = resolveAutoTrack(options.autoTrack);
|
|
412
738
|
const opts = {
|
|
739
|
+
appId: options.appId,
|
|
413
740
|
publicKey: options.publicKey,
|
|
741
|
+
environment: options.environment,
|
|
414
742
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
415
743
|
persistIdentity,
|
|
416
744
|
storagePrefix: options.storagePrefix ?? "crossdeck:",
|
|
417
745
|
autoHeartbeat: options.autoHeartbeat ?? true,
|
|
418
746
|
eventFlushBatchSize: options.eventFlushBatchSize ?? 20,
|
|
419
747
|
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 5e3,
|
|
420
|
-
sdkVersion: options.sdkVersion ?? SDK_VERSION
|
|
748
|
+
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
749
|
+
autoTrack,
|
|
750
|
+
appVersion: options.appVersion ?? null
|
|
421
751
|
};
|
|
752
|
+
const debug = new ConsoleDebugLogger();
|
|
753
|
+
debug.enabled = options.debug === true;
|
|
422
754
|
const http = new HttpClient({
|
|
423
755
|
publicKey: opts.publicKey,
|
|
424
756
|
baseUrl: opts.baseUrl,
|
|
@@ -430,20 +762,62 @@ var CrossdeckClient = class {
|
|
|
430
762
|
const events = new EventQueue({
|
|
431
763
|
http,
|
|
432
764
|
batchSize: opts.eventFlushBatchSize,
|
|
433
|
-
intervalMs: opts.eventFlushIntervalMs
|
|
765
|
+
intervalMs: opts.eventFlushIntervalMs,
|
|
766
|
+
envelope: () => ({
|
|
767
|
+
appId: opts.appId,
|
|
768
|
+
environment: opts.environment,
|
|
769
|
+
sdk: { name: SDK_NAME, version: opts.sdkVersion }
|
|
770
|
+
}),
|
|
771
|
+
onFirstFlushSuccess: () => {
|
|
772
|
+
debug.emit(
|
|
773
|
+
"sdk.first_event_sent",
|
|
774
|
+
"First telemetry event received. View it in Live Events.",
|
|
775
|
+
{ appId: opts.appId, environment: opts.environment }
|
|
776
|
+
);
|
|
777
|
+
}
|
|
434
778
|
});
|
|
779
|
+
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
435
780
|
this.state = {
|
|
436
781
|
http,
|
|
437
782
|
identity,
|
|
438
783
|
entitlements,
|
|
439
784
|
events,
|
|
785
|
+
autoTracker: null,
|
|
786
|
+
deviceInfo,
|
|
440
787
|
options: opts,
|
|
788
|
+
debug,
|
|
441
789
|
developerUserId: null
|
|
442
790
|
};
|
|
791
|
+
debug.emit("sdk.configured", `Crossdeck connected to ${opts.appId} in ${opts.environment} mode.`, {
|
|
792
|
+
appId: opts.appId,
|
|
793
|
+
environment: opts.environment,
|
|
794
|
+
sdkVersion: opts.sdkVersion
|
|
795
|
+
});
|
|
796
|
+
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
797
|
+
const tracker = new AutoTracker(
|
|
798
|
+
autoTrack,
|
|
799
|
+
(name, properties) => this.track(name, properties)
|
|
800
|
+
);
|
|
801
|
+
this.state.autoTracker = tracker;
|
|
802
|
+
tracker.install();
|
|
803
|
+
}
|
|
443
804
|
if (opts.autoHeartbeat) {
|
|
444
805
|
void this.heartbeat().catch(() => void 0);
|
|
445
806
|
}
|
|
446
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
810
|
+
* lifecycle method name across SDKs as `init` (formerly `start` /
|
|
811
|
+
* `configure`). `start` will be removed in a future major version.
|
|
812
|
+
*/
|
|
813
|
+
start(options) {
|
|
814
|
+
if (typeof console !== "undefined") {
|
|
815
|
+
console.warn(
|
|
816
|
+
"[crossdeck] Crossdeck.start() is deprecated \u2014 use Crossdeck.init() instead. The signature is the same."
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this.init(options);
|
|
820
|
+
}
|
|
447
821
|
/**
|
|
448
822
|
* Link the anonymous device to a developer-supplied user ID. Cache
|
|
449
823
|
* the resolved Crossdeck customer for follow-up calls.
|
|
@@ -499,7 +873,7 @@ var CrossdeckClient = class {
|
|
|
499
873
|
/**
|
|
500
874
|
* Queue a telemetry event. Returns immediately — the network round-
|
|
501
875
|
* trip happens in the background. To flush before the page unloads,
|
|
502
|
-
* call
|
|
876
|
+
* call flush().
|
|
503
877
|
*/
|
|
504
878
|
track(name, properties) {
|
|
505
879
|
const s = this.requireStarted();
|
|
@@ -510,37 +884,97 @@ var CrossdeckClient = class {
|
|
|
510
884
|
message: "track(name) requires a non-empty name."
|
|
511
885
|
});
|
|
512
886
|
}
|
|
887
|
+
if (s.debug.enabled && properties) {
|
|
888
|
+
const flagged = findSensitivePropertyKeys(properties);
|
|
889
|
+
if (flagged.length > 0) {
|
|
890
|
+
s.debug.emit(
|
|
891
|
+
"sdk.sensitive_property_warning",
|
|
892
|
+
`Event "${name}" has potentially sensitive property names: ${flagged.join(", ")}. Crossdeck is privacy-first \u2014 avoid sending PII unless intentional.`,
|
|
893
|
+
{ eventName: name, flagged }
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (s.debug.enabled && !s.developerUserId && !s.identity.crossdeckCustomerId) {
|
|
898
|
+
s.debug.emit(
|
|
899
|
+
"sdk.no_identity",
|
|
900
|
+
"Using anonymous user until identify(userId) is called."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const enriched = { ...s.deviceInfo };
|
|
904
|
+
const sessionId = s.autoTracker?.currentSessionId;
|
|
905
|
+
if (sessionId) enriched.sessionId = sessionId;
|
|
906
|
+
if (properties) Object.assign(enriched, properties);
|
|
513
907
|
const event = {
|
|
514
908
|
eventId: this.mintEventId(),
|
|
515
909
|
name,
|
|
516
910
|
timestamp: Date.now(),
|
|
517
|
-
properties:
|
|
911
|
+
properties: enriched
|
|
518
912
|
};
|
|
519
913
|
Object.assign(event, this.identityHintForEvent());
|
|
520
914
|
s.events.enqueue(event);
|
|
521
915
|
}
|
|
522
|
-
/**
|
|
523
|
-
|
|
916
|
+
/**
|
|
917
|
+
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
918
|
+
*
|
|
919
|
+
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
920
|
+
*/
|
|
921
|
+
async flush() {
|
|
524
922
|
const s = this.requireStarted();
|
|
525
923
|
await s.events.flush();
|
|
526
924
|
}
|
|
527
|
-
/**
|
|
528
|
-
async
|
|
925
|
+
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
926
|
+
async flushEvents() {
|
|
927
|
+
return this.flush();
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Forward purchase evidence to the backend for verification + entitlement
|
|
931
|
+
* projection. NorthStar §4 + §13 canonical name.
|
|
932
|
+
*
|
|
933
|
+
* Today the web SDK only supports Apple StoreKit 2 forwarding (web apps
|
|
934
|
+
* that sit alongside an iOS app). Stripe doesn't need this method —
|
|
935
|
+
* Stripe webhooks deliver evidence server-side without a client round-trip.
|
|
936
|
+
*/
|
|
937
|
+
async syncPurchases(input) {
|
|
529
938
|
const s = this.requireStarted();
|
|
530
939
|
if (!input.signedTransactionInfo) {
|
|
531
940
|
throw new CrossdeckError({
|
|
532
941
|
type: "invalid_request_error",
|
|
533
942
|
code: "missing_signed_transaction_info",
|
|
534
|
-
message: "
|
|
943
|
+
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
535
944
|
});
|
|
536
945
|
}
|
|
537
|
-
const result = await s.http.request("POST", "/purchases", {
|
|
538
|
-
body: { rail: "apple", ...input }
|
|
946
|
+
const result = await s.http.request("POST", "/purchases/sync", {
|
|
947
|
+
body: { rail: input.rail ?? "apple", ...input }
|
|
539
948
|
});
|
|
540
949
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
541
950
|
s.entitlements.setFromList(result.entitlements);
|
|
951
|
+
s.debug.emit(
|
|
952
|
+
"sdk.purchase_evidence_sent",
|
|
953
|
+
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
954
|
+
{ rail: input.rail ?? "apple" }
|
|
955
|
+
);
|
|
542
956
|
return result;
|
|
543
957
|
}
|
|
958
|
+
/** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */
|
|
959
|
+
async purchaseApple(input) {
|
|
960
|
+
return this.syncPurchases({ rail: "apple", ...input });
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Toggle verbose diagnostic logging — NorthStar §16. When enabled, the
|
|
964
|
+
* SDK emits a fixed vocabulary of debug signals to console.info that the
|
|
965
|
+
* dashboard's onboarding checklist can also surface as live events.
|
|
966
|
+
*/
|
|
967
|
+
setDebugMode(enabled) {
|
|
968
|
+
const s = this.requireStarted();
|
|
969
|
+
s.debug.enabled = enabled;
|
|
970
|
+
if (enabled) {
|
|
971
|
+
s.debug.emit(
|
|
972
|
+
"sdk.configured",
|
|
973
|
+
`Debug mode enabled for ${s.options.appId} in ${s.options.environment} mode.`,
|
|
974
|
+
{ appId: s.options.appId, environment: s.options.environment }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
544
978
|
/**
|
|
545
979
|
* Send the boot heartbeat. Called automatically by start() unless
|
|
546
980
|
* autoHeartbeat:false. Safe to call manually as a "we're still here" ping.
|
|
@@ -556,10 +990,19 @@ var CrossdeckClient = class {
|
|
|
556
990
|
*/
|
|
557
991
|
reset() {
|
|
558
992
|
if (!this.state) return;
|
|
993
|
+
this.state.autoTracker?.uninstall();
|
|
559
994
|
this.state.identity.reset();
|
|
560
995
|
this.state.entitlements.clear();
|
|
561
996
|
this.state.events.reset();
|
|
562
997
|
this.state.developerUserId = null;
|
|
998
|
+
if (this.state.autoTracker) {
|
|
999
|
+
const tracker = new AutoTracker(
|
|
1000
|
+
this.state.options.autoTrack,
|
|
1001
|
+
(name, props) => this.track(name, props)
|
|
1002
|
+
);
|
|
1003
|
+
this.state.autoTracker = tracker;
|
|
1004
|
+
tracker.install();
|
|
1005
|
+
}
|
|
563
1006
|
}
|
|
564
1007
|
/**
|
|
565
1008
|
* Diagnostic: current state + queue stats. Useful for the dashboard's
|
|
@@ -608,8 +1051,8 @@ var CrossdeckClient = class {
|
|
|
608
1051
|
if (!this.state) {
|
|
609
1052
|
throw new CrossdeckError({
|
|
610
1053
|
type: "configuration_error",
|
|
611
|
-
code: "
|
|
612
|
-
message: "Call Crossdeck.
|
|
1054
|
+
code: "not_initialized",
|
|
1055
|
+
message: "Call Crossdeck.init({ appId, publicKey, environment }) before any other method."
|
|
613
1056
|
});
|
|
614
1057
|
}
|
|
615
1058
|
return this.state;
|
|
@@ -642,6 +1085,24 @@ var CrossdeckClient = class {
|
|
|
642
1085
|
}
|
|
643
1086
|
};
|
|
644
1087
|
var Crossdeck = new CrossdeckClient();
|
|
1088
|
+
function inferEnvFromKey(publicKey) {
|
|
1089
|
+
if (publicKey.startsWith("cd_pub_test_")) return "sandbox";
|
|
1090
|
+
if (publicKey.startsWith("cd_pub_live_")) return "production";
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
function resolveAutoTrack(input) {
|
|
1094
|
+
if (input === false) {
|
|
1095
|
+
return { sessions: false, pageViews: false, deviceInfo: false };
|
|
1096
|
+
}
|
|
1097
|
+
if (input === void 0 || input === true) {
|
|
1098
|
+
return { ...DEFAULT_AUTO_TRACK };
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
sessions: input.sessions ?? DEFAULT_AUTO_TRACK.sessions,
|
|
1102
|
+
pageViews: input.pageViews ?? DEFAULT_AUTO_TRACK.pageViews,
|
|
1103
|
+
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
645
1106
|
// Annotate the CommonJS export names for ESM import in node:
|
|
646
1107
|
0 && (module.exports = {
|
|
647
1108
|
Crossdeck,
|