@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/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.1.1";
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: { events: batch }
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 start twice with the same options
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
- start(options) {
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.start requires a publishable key starting with cd_pub_."
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 flushEvents().
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: properties ?? {}
911
+ properties: enriched
518
912
  };
519
913
  Object.assign(event, this.identityHintForEvent());
520
914
  s.events.enqueue(event);
521
915
  }
522
- /** Force-flush queued events. Useful to call from page-unload handlers. */
523
- async flushEvents() {
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
- /** Forward an Apple StoreKit 2 transaction for verification + projection. */
528
- async purchaseApple(input) {
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: "purchaseApple requires a signedTransactionInfo string from StoreKit 2."
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: "not_started",
612
- message: "Call Crossdeck.start({ publicKey }) before any other method."
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,