@pulseboard/react-native 0.2.1 → 0.2.3

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
@@ -1,4 +1,5 @@
1
1
  import { TurboModuleRegistry, Dimensions, Platform, I18nManager } from 'react-native';
2
+ import AsyncStorage from '@react-native-async-storage/async-storage';
2
3
 
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
5
  var __commonJS = (cb, mod) => function __require() {
@@ -376,6 +377,60 @@ var PulseBoardClient = class {
376
377
  return false;
377
378
  }
378
379
  }
380
+ // ─── Log Entry ─────────────────────────────────────────────
381
+ async sendLog(entry) {
382
+ try {
383
+ const response = await fetch(`${this.host}/logs`, {
384
+ method: "POST",
385
+ headers: { "Content-Type": "application/json" },
386
+ body: JSON.stringify(entry)
387
+ });
388
+ if (!response.ok) {
389
+ this.log(`Failed to send log: ${response.status}`);
390
+ return false;
391
+ }
392
+ return true;
393
+ } catch (err) {
394
+ this.log(`Network error sending log: ${err}`);
395
+ return false;
396
+ }
397
+ }
398
+ async sendLogBatch(apiKey, logs) {
399
+ try {
400
+ const response = await fetch(`${this.host}/logs/batch`, {
401
+ method: "POST",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify({ apiKey, logs })
404
+ });
405
+ if (!response.ok) {
406
+ this.log(`Failed to send log batch: ${response.status}`);
407
+ return false;
408
+ }
409
+ return true;
410
+ } catch (err) {
411
+ this.log(`Network error sending log batch: ${err}`);
412
+ return false;
413
+ }
414
+ }
415
+ // ─── Feedback ─────────────────────────────────────────────────────
416
+ async sendFeedback(payload) {
417
+ try {
418
+ const response = await fetch(`${this.host}/feedback`, {
419
+ method: "POST",
420
+ headers: { "Content-Type": "application/json" },
421
+ body: JSON.stringify(payload)
422
+ });
423
+ if (!response.ok) {
424
+ this.log(`Failed to send feedback: ${response.status}`);
425
+ return false;
426
+ }
427
+ this.log("Feedback sent");
428
+ return true;
429
+ } catch (err) {
430
+ this.log(`Network error sending feedback: ${err}`);
431
+ return false;
432
+ }
433
+ }
379
434
  // ─── Private ──────────────────────────────────────────────────────
380
435
  chunk(arr, size) {
381
436
  return Array.from(
@@ -491,30 +546,63 @@ var ContextCollector = class {
491
546
  };
492
547
  }
493
548
  };
494
-
495
- // src/queue.ts
549
+ var QUEUE_KEY = "@pulseboard/event_queue";
550
+ var MAX_QUEUE_SIZE = 100;
496
551
  var EventQueue = class {
497
- constructor(maxSize = 100) {
498
- this.queue = [];
552
+ constructor(maxSize = MAX_QUEUE_SIZE) {
553
+ this.memoryQueue = [];
554
+ this._hydrated = false;
499
555
  this.maxSize = maxSize;
556
+ this.hydrate();
557
+ }
558
+ // ─── Hydrate from storage on init ─────────────────────────────────
559
+ async hydrate() {
560
+ try {
561
+ const raw = await AsyncStorage.getItem(QUEUE_KEY);
562
+ if (raw) {
563
+ const stored = JSON.parse(raw);
564
+ this.memoryQueue = stored.slice(0, this.maxSize);
565
+ }
566
+ } catch {
567
+ } finally {
568
+ this._hydrated = true;
569
+ }
570
+ }
571
+ // ─── Persist to storage ────────────────────────────────────────────
572
+ async persist() {
573
+ try {
574
+ await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.memoryQueue));
575
+ } catch {
576
+ }
500
577
  }
578
+ // ─── Public API ────────────────────────────────────────────────────
501
579
  enqueue(event) {
502
- if (this.queue.length >= this.maxSize) {
503
- this.queue.shift();
580
+ if (this.memoryQueue.length >= this.maxSize) {
581
+ this.memoryQueue.shift();
504
582
  }
505
- this.queue.push(event);
583
+ this.memoryQueue.push(event);
584
+ this.persist();
506
585
  }
507
586
  dequeue(count) {
508
- return this.queue.splice(0, count);
587
+ const batch = this.memoryQueue.splice(0, count);
588
+ this.persist();
589
+ return batch;
509
590
  }
510
591
  get size() {
511
- return this.queue.length;
592
+ return this.memoryQueue.length;
512
593
  }
513
594
  get isEmpty() {
514
- return this.queue.length === 0;
595
+ return this.memoryQueue.length === 0;
596
+ }
597
+ get hydrated() {
598
+ return this._hydrated;
515
599
  }
516
- clear() {
517
- this.queue = [];
600
+ async clear() {
601
+ this.memoryQueue = [];
602
+ try {
603
+ await AsyncStorage.removeItem(QUEUE_KEY);
604
+ } catch {
605
+ }
518
606
  }
519
607
  };
520
608
 
@@ -529,25 +617,70 @@ var PulseBoardSDK = class {
529
617
  this.flushTimer = null;
530
618
  this.initialized = false;
531
619
  this.PULSEBOARD_API = "https://pulseboard-production.up.railway.app/";
620
+ // ─── Log queue ────────────────────────────────────────────────────
621
+ this.logQueue = [];
622
+ this.logFlushTimer = null;
623
+ this.originalConsole = {};
624
+ this.consoleCapturing = false;
625
+ }
626
+ get apiKey() {
627
+ return this.config?.apiKey ?? "";
532
628
  }
533
629
  // ─── Public API ───────────────────────────────────────────────────
534
630
  init(config) {
535
631
  if (this.initialized) {
536
- this.log("SDK already initialized \u2014 skipping");
632
+ this.debugLog("SDK already initialized \u2014 skipping");
537
633
  return;
538
634
  }
635
+ const hasDirectKey = !!config.apiKey;
636
+ const hasIdentifiers = !!(config.organisation && config.product && config.project);
637
+ if (!hasDirectKey && !hasIdentifiers) {
638
+ throw new Error(
639
+ "[PulseBoard] init() requires either apiKey or (organisation + product + project)"
640
+ );
641
+ }
539
642
  this.config = {
540
643
  autoCapture: true,
541
644
  debug: false,
542
645
  flushInterval: 5e3,
543
646
  maxQueueSize: 100,
544
- ...config
647
+ ...config,
648
+ apiKey: config.apiKey ?? ""
649
+ // resolved below if using identifiers
545
650
  };
546
651
  this.client = new PulseBoardClient(this.PULSEBOARD_API, this.config.debug);
547
652
  this.queue = new EventQueue(this.config.maxQueueSize);
548
653
  this.contextCollector = new ContextCollector({
549
654
  environment: config.environment
550
655
  });
656
+ if (!hasDirectKey && hasIdentifiers) {
657
+ this.resolveApiKey(
658
+ config.organisation,
659
+ config.product,
660
+ config.project
661
+ );
662
+ } else {
663
+ this.completeInit();
664
+ }
665
+ }
666
+ async resolveApiKey(organisation, product, project) {
667
+ try {
668
+ const response = await fetch(
669
+ `${this.PULSEBOARD_API}sdk/resolve?organisation=${encodeURIComponent(organisation)}&product=${encodeURIComponent(product)}&project=${encodeURIComponent(project)}`
670
+ );
671
+ if (!response.ok) {
672
+ throw new Error(
673
+ `[PulseBoard] Could not resolve project. Check your organisation, product and project values.`
674
+ );
675
+ }
676
+ const data = await response.json();
677
+ this.config.apiKey = data.data.apiKey;
678
+ this.completeInit();
679
+ } catch (err) {
680
+ console.error("[PulseBoard] Failed to resolve API key:", err);
681
+ }
682
+ }
683
+ completeInit() {
551
684
  this.flushTimer = setInterval(
552
685
  () => this.flush(),
553
686
  this.config.flushInterval
@@ -559,7 +692,7 @@ var PulseBoardSDK = class {
559
692
  this.autoCapture.attach();
560
693
  }
561
694
  this.initialized = true;
562
- this.log(`Initialized \u2014 host: ${this.PULSEBOARD_API}`);
695
+ this.debugLog(`Initialized \u2014 host: ${this.PULSEBOARD_API}`);
563
696
  }
564
697
  async getContext() {
565
698
  this.assertInitialized("getContext");
@@ -568,7 +701,7 @@ var PulseBoardSDK = class {
568
701
  identify(user) {
569
702
  this.assertInitialized("identify");
570
703
  this.contextCollector.identify(user);
571
- this.log(`User identified: ${JSON.stringify(user)}`);
704
+ this.debugLog(`User identified: ${JSON.stringify(user)}`);
572
705
  }
573
706
  clearUser() {
574
707
  this.assertInitialized("clearUser");
@@ -600,26 +733,87 @@ var PulseBoardSDK = class {
600
733
  ...options.payload
601
734
  });
602
735
  }
736
+ // ─── Logging API ──────────────────────────────────────────────────
737
+ log(level, message, options = {}) {
738
+ this.assertInitialized("log");
739
+ this.contextCollector.collect().then((context) => {
740
+ this.logQueue.push({
741
+ level,
742
+ message,
743
+ meta: options.meta,
744
+ sessionId: context.session.sessionId,
745
+ appVersion: context.device.appVersion,
746
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
747
+ });
748
+ if (level === "error") this.flushLogs();
749
+ }).catch(() => {
750
+ });
751
+ }
752
+ captureConsole() {
753
+ this.assertInitialized("captureConsole");
754
+ if (this.consoleCapturing) return;
755
+ this.originalConsole = {
756
+ log: console.log,
757
+ warn: console.warn,
758
+ error: console.error,
759
+ debug: console.debug
760
+ };
761
+ console.log = (...args) => {
762
+ this.originalConsole.log?.(...args);
763
+ this.log("info", args.map(String).join(" "));
764
+ };
765
+ console.warn = (...args) => {
766
+ this.originalConsole.warn?.(...args);
767
+ this.log("warn", args.map(String).join(" "));
768
+ };
769
+ console.error = (...args) => {
770
+ this.originalConsole.error?.(...args);
771
+ this.log("error", args.map(String).join(" "));
772
+ };
773
+ console.debug = (...args) => {
774
+ this.originalConsole.debug?.(...args);
775
+ this.log("debug", args.map(String).join(" "));
776
+ };
777
+ this.logFlushTimer = setInterval(() => this.flushLogs(), 1e4);
778
+ this.consoleCapturing = true;
779
+ this.debugLog("Console capture enabled");
780
+ }
781
+ releaseConsole() {
782
+ if (!this.consoleCapturing) return;
783
+ if (this.originalConsole.log) console.log = this.originalConsole.log;
784
+ if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
785
+ if (this.originalConsole.error) console.error = this.originalConsole.error;
786
+ if (this.originalConsole.debug) console.debug = this.originalConsole.debug;
787
+ if (this.logFlushTimer) {
788
+ clearInterval(this.logFlushTimer);
789
+ this.logFlushTimer = null;
790
+ }
791
+ this.originalConsole = {};
792
+ this.consoleCapturing = false;
793
+ }
794
+ async flushLogs() {
795
+ if (!this.client || !this.config || this.logQueue.length === 0) return;
796
+ const batch = this.logQueue.splice(0, this.logQueue.length);
797
+ await this.client.sendLogBatch(this.apiKey, batch);
798
+ }
603
799
  // ─── Analytics API ────────────────────────────────────────────────
604
800
  startSession() {
605
801
  this.assertInitialized("startSession");
606
802
  this.contextCollector.collect().then((context) => {
607
803
  this.client.sendAnalytics("session", {
608
- apiKey: this.config.apiKey,
804
+ apiKey: this.apiKey,
609
805
  sessionId: context.session.sessionId,
610
806
  startedAt: context.session.startedAt,
611
807
  context
612
808
  });
613
- this.log(`Session started: ${context.session.sessionId}`);
614
- }).catch((err) => {
615
- this.log(`Failed to start session: ${err}`);
616
- });
809
+ this.debugLog(`Session started: ${context.session.sessionId}`);
810
+ }).catch((err) => this.debugLog(`Failed to start session: ${err}`));
617
811
  }
618
812
  endSession(duration) {
619
813
  this.assertInitialized("endSession");
620
814
  this.contextCollector.collect().then((context) => {
621
815
  this.client.sendAnalytics("session", {
622
- apiKey: this.config.apiKey,
816
+ apiKey: this.apiKey,
623
817
  sessionId: context.session.sessionId,
624
818
  startedAt: context.session.startedAt,
625
819
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -627,31 +821,27 @@ var PulseBoardSDK = class {
627
821
  context
628
822
  });
629
823
  this.flush();
630
- this.log(`Session ended: ${context.session.sessionId}`);
631
- }).catch((err) => {
632
- this.log(`Failed to end session: ${err}`);
633
- });
824
+ this.debugLog(`Session ended: ${context.session.sessionId}`);
825
+ }).catch((err) => this.debugLog(`Failed to end session: ${err}`));
634
826
  }
635
827
  trackScreen(screenName, loadTime) {
636
828
  this.assertInitialized("trackScreen");
637
829
  this.contextCollector.collect().then((context) => {
638
830
  this.client.sendAnalytics("screen-view", {
639
- apiKey: this.config.apiKey,
831
+ apiKey: this.apiKey,
640
832
  screenName,
641
833
  loadTime,
642
834
  sessionId: context.session.sessionId,
643
835
  context
644
836
  });
645
- this.log(`Screen tracked: ${screenName}`);
646
- }).catch((err) => {
647
- this.log(`Failed to track screen: ${err}`);
648
- });
837
+ this.debugLog(`Screen tracked: ${screenName}`);
838
+ }).catch((err) => this.debugLog(`Failed to track screen: ${err}`));
649
839
  }
650
840
  trackApiCall(endpoint, httpMethod, statusCode, duration, payloadSize) {
651
841
  this.assertInitialized("trackApiCall");
652
842
  this.contextCollector.collect().then((context) => {
653
843
  this.client.sendAnalytics("api-call", {
654
- apiKey: this.config.apiKey,
844
+ apiKey: this.apiKey,
655
845
  endpoint,
656
846
  httpMethod,
657
847
  statusCode,
@@ -660,16 +850,16 @@ var PulseBoardSDK = class {
660
850
  sessionId: context.session.sessionId,
661
851
  context
662
852
  });
663
- this.log(`API call tracked: ${httpMethod} ${endpoint} ${statusCode}`);
664
- }).catch((err) => {
665
- this.log(`Failed to track API call: ${err}`);
666
- });
853
+ this.debugLog(
854
+ `API call tracked: ${httpMethod} ${endpoint} ${statusCode}`
855
+ );
856
+ }).catch((err) => this.debugLog(`Failed to track API call: ${err}`));
667
857
  }
668
858
  trackCrash(error, isFatal = false) {
669
859
  this.assertInitialized("trackCrash");
670
860
  this.contextCollector.collect().then((context) => {
671
861
  this.client.sendAnalytics("crash", {
672
- apiKey: this.config.apiKey,
862
+ apiKey: this.apiKey,
673
863
  errorName: error.name,
674
864
  errorMessage: error.message,
675
865
  stackTrace: error.stack ?? "",
@@ -677,17 +867,46 @@ var PulseBoardSDK = class {
677
867
  sessionId: context.session.sessionId,
678
868
  context
679
869
  });
680
- this.log(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
681
- }).catch((err) => {
682
- this.log(`Failed to track crash: ${err}`);
683
- });
870
+ this.debugLog(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
871
+ }).catch((err) => this.debugLog(`Failed to track crash: ${err}`));
684
872
  }
685
- // ─── Flush & Destroy ─────────────────────────────────────────────
873
+ feedback(message, options = {}) {
874
+ this.assertInitialized("feedback");
875
+ this.contextCollector.collect().then((context) => {
876
+ this.client.sendFeedback({
877
+ apiKey: this.apiKey,
878
+ type: options.type ?? "general",
879
+ message,
880
+ meta: options.meta,
881
+ userEmail: options.userEmail,
882
+ userName: options.userName,
883
+ screenshot: options.screenshot,
884
+ sessionId: context.session.sessionId,
885
+ appVersion: context.device.appVersion
886
+ });
887
+ this.debugLog(`Feedback sent: ${options.type ?? "general"}`);
888
+ }).catch((err) => this.debugLog(`Failed to send feedback: ${err}`));
889
+ }
890
+ // ─── Flush & Destroy ──────────────────────────────────────────────
686
891
  async flush() {
687
- if (!this.queue || this.queue.isEmpty) return;
688
- if (!this.client) return;
892
+ if (!this.queue || !this.client) return;
893
+ if (!this.queue.hydrated) {
894
+ await new Promise((resolve) => {
895
+ const interval = setInterval(() => {
896
+ if (this.queue?.hydrated) {
897
+ clearInterval(interval);
898
+ resolve();
899
+ }
900
+ }, 50);
901
+ setTimeout(() => {
902
+ clearInterval(interval);
903
+ resolve();
904
+ }, 500);
905
+ });
906
+ }
907
+ if (this.queue.isEmpty) return;
689
908
  const events = this.queue.dequeue(10);
690
- this.log(`Flushing ${events.length} event(s)`);
909
+ this.debugLog(`Flushing ${events.length} event(s)`);
691
910
  await this.client.sendBatch(events);
692
911
  }
693
912
  destroy() {
@@ -695,20 +914,22 @@ var PulseBoardSDK = class {
695
914
  clearInterval(this.flushTimer);
696
915
  this.flushTimer = null;
697
916
  }
917
+ this.releaseConsole();
698
918
  this.autoCapture?.detach();
699
919
  this.queue?.clear();
920
+ this.logQueue = [];
700
921
  this.initialized = false;
701
922
  this.config = null;
702
923
  this.client = null;
703
924
  this.queue = null;
704
925
  this.contextCollector = null;
705
- this.log("SDK destroyed");
926
+ this.debugLog("SDK destroyed");
706
927
  }
707
- // ─── Private ─────────────────────────────────────────────────────
928
+ // ─── Private ──────────────────────────────────────────────────────
708
929
  buildAndEnqueue(type, name, payload, timestamp) {
709
930
  this.contextCollector.collect().then((context) => {
710
931
  const event = {
711
- apiKey: this.config.apiKey,
932
+ apiKey: this.apiKey,
712
933
  type,
713
934
  name,
714
935
  payload,
@@ -716,20 +937,16 @@ var PulseBoardSDK = class {
716
937
  context
717
938
  };
718
939
  this.queue.enqueue(event);
719
- this.log(`Queued: ${type} \u2014 ${name}`);
720
- if (type === "error") {
721
- this.flush();
722
- }
723
- }).catch((err) => {
724
- this.log(`Failed to collect context: ${err}`);
725
- });
940
+ this.debugLog(`Queued: ${type} \u2014 ${name}`);
941
+ if (type === "error") this.flush();
942
+ }).catch((err) => this.debugLog(`Failed to collect context: ${err}`));
726
943
  }
727
944
  assertInitialized(method) {
728
945
  if (!this.initialized) {
729
946
  throw new Error(`PulseBoard.${method}() called before PulseBoard.init()`);
730
947
  }
731
948
  }
732
- log(message) {
949
+ debugLog(message) {
733
950
  if (this.config?.debug) {
734
951
  console.log(`[PulseBoard] ${message}`);
735
952
  }