@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.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.1.1";
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: { events: batch }
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 start twice with the same options
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
- start(options) {
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.start requires a publishable key starting with cd_pub_."
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 flushEvents().
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: properties ?? {}
879
+ properties: enriched
486
880
  };
487
881
  Object.assign(event, this.identityHintForEvent());
488
882
  s.events.enqueue(event);
489
883
  }
490
- /** Force-flush queued events. Useful to call from page-unload handlers. */
491
- async flushEvents() {
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
- /** Forward an Apple StoreKit 2 transaction for verification + projection. */
496
- async purchaseApple(input) {
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: "purchaseApple requires a signedTransactionInfo string from StoreKit 2."
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: "not_started",
580
- message: "Call Crossdeck.start({ publicKey }) before any other method."
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,