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