@pulseboard/react-native 0.2.0 → 0.2.2

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() {
@@ -285,6 +286,42 @@ var require_rejection_tracking = __commonJS({
285
286
  }
286
287
  });
287
288
 
289
+ // src/auto-capture.ts
290
+ var AutoCapture = class {
291
+ constructor(handler) {
292
+ this.attached = false;
293
+ this.previousHandler = null;
294
+ this.handler = handler;
295
+ }
296
+ attach() {
297
+ if (this.attached) return;
298
+ this.previousHandler = ErrorUtils.getGlobalHandler();
299
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
300
+ this.handler("uncaughtException", error, { isFatal: isFatal ?? false });
301
+ if (this.previousHandler) {
302
+ this.previousHandler(error, isFatal);
303
+ }
304
+ });
305
+ const tracking = require_rejection_tracking();
306
+ tracking.enable({
307
+ allRejections: true,
308
+ onUnhandled: (_id, error) => {
309
+ const err = error instanceof Error ? error : new Error(String(error));
310
+ this.handler("unhandledRejection", err, { fatal: false });
311
+ }
312
+ });
313
+ this.attached = true;
314
+ }
315
+ detach() {
316
+ if (!this.attached) return;
317
+ if (this.previousHandler) {
318
+ ErrorUtils.setGlobalHandler(this.previousHandler);
319
+ this.previousHandler = null;
320
+ }
321
+ this.attached = false;
322
+ }
323
+ };
324
+
288
325
  // src/client.ts
289
326
  var PulseBoardClient = class {
290
327
  constructor(host, debug = false) {
@@ -340,6 +377,60 @@ var PulseBoardClient = class {
340
377
  return false;
341
378
  }
342
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
+ }
343
434
  // ─── Private ──────────────────────────────────────────────────────
344
435
  chunk(arr, size) {
345
436
  return Array.from(
@@ -353,68 +444,6 @@ var PulseBoardClient = class {
353
444
  }
354
445
  }
355
446
  };
356
-
357
- // src/queue.ts
358
- var EventQueue = class {
359
- constructor(maxSize = 100) {
360
- this.queue = [];
361
- this.maxSize = maxSize;
362
- }
363
- enqueue(event) {
364
- if (this.queue.length >= this.maxSize) {
365
- this.queue.shift();
366
- }
367
- this.queue.push(event);
368
- }
369
- dequeue(count) {
370
- return this.queue.splice(0, count);
371
- }
372
- get size() {
373
- return this.queue.length;
374
- }
375
- get isEmpty() {
376
- return this.queue.length === 0;
377
- }
378
- clear() {
379
- this.queue = [];
380
- }
381
- };
382
-
383
- // src/auto-capture.ts
384
- var AutoCapture = class {
385
- constructor(handler) {
386
- this.attached = false;
387
- this.previousHandler = null;
388
- this.handler = handler;
389
- }
390
- attach() {
391
- if (this.attached) return;
392
- this.previousHandler = ErrorUtils.getGlobalHandler();
393
- ErrorUtils.setGlobalHandler((error, isFatal) => {
394
- this.handler("uncaughtException", error, { isFatal: isFatal ?? false });
395
- if (this.previousHandler) {
396
- this.previousHandler(error, isFatal);
397
- }
398
- });
399
- const tracking = require_rejection_tracking();
400
- tracking.enable({
401
- allRejections: true,
402
- onUnhandled: (_id, error) => {
403
- const err = error instanceof Error ? error : new Error(String(error));
404
- this.handler("unhandledRejection", err, { fatal: false });
405
- }
406
- });
407
- this.attached = true;
408
- }
409
- detach() {
410
- if (!this.attached) return;
411
- if (this.previousHandler) {
412
- ErrorUtils.setGlobalHandler(this.previousHandler);
413
- this.previousHandler = null;
414
- }
415
- this.attached = false;
416
- }
417
- };
418
447
  var NativePulseBoardDevice_default = TurboModuleRegistry.getEnforcing("PulseBoardDevice");
419
448
  var NativePulseBoardNetwork_default = TurboModuleRegistry.getEnforcing("PulseBoardNetwork");
420
449
 
@@ -491,8 +520,8 @@ var ContextCollector = class {
491
520
  manufacturer: deviceInfo?.manufacturer ?? "unknown",
492
521
  brand: deviceInfo?.brand ?? "unknown",
493
522
  isTablet: deviceInfo?.isTablet ?? false,
494
- appVersion: this.appContext.appVersion ?? deviceInfo?.appVersion ?? "unknown",
495
- buildNumber: this.appContext.buildNumber ?? deviceInfo?.buildNumber ?? "unknown",
523
+ appVersion: deviceInfo?.appVersion ?? "unknown",
524
+ buildNumber: deviceInfo?.buildNumber ?? "unknown",
496
525
  bundleId: deviceInfo?.bundleId ?? "unknown",
497
526
  screenWidth: deviceInfo?.screenWidth ?? width,
498
527
  screenHeight: deviceInfo?.screenHeight ?? height,
@@ -517,6 +546,65 @@ var ContextCollector = class {
517
546
  };
518
547
  }
519
548
  };
549
+ var QUEUE_KEY = "@pulseboard/event_queue";
550
+ var MAX_QUEUE_SIZE = 100;
551
+ var EventQueue = class {
552
+ constructor(maxSize = MAX_QUEUE_SIZE) {
553
+ this.memoryQueue = [];
554
+ this._hydrated = false;
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
+ }
577
+ }
578
+ // ─── Public API ────────────────────────────────────────────────────
579
+ enqueue(event) {
580
+ if (this.memoryQueue.length >= this.maxSize) {
581
+ this.memoryQueue.shift();
582
+ }
583
+ this.memoryQueue.push(event);
584
+ this.persist();
585
+ }
586
+ dequeue(count) {
587
+ const batch = this.memoryQueue.splice(0, count);
588
+ this.persist();
589
+ return batch;
590
+ }
591
+ get size() {
592
+ return this.memoryQueue.length;
593
+ }
594
+ get isEmpty() {
595
+ return this.memoryQueue.length === 0;
596
+ }
597
+ get hydrated() {
598
+ return this._hydrated;
599
+ }
600
+ async clear() {
601
+ this.memoryQueue = [];
602
+ try {
603
+ await AsyncStorage.removeItem(QUEUE_KEY);
604
+ } catch {
605
+ }
606
+ }
607
+ };
520
608
 
521
609
  // src/index.ts
522
610
  var PulseBoardSDK = class {
@@ -528,23 +616,71 @@ var PulseBoardSDK = class {
528
616
  this.contextCollector = null;
529
617
  this.flushTimer = null;
530
618
  this.initialized = false;
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 ?? "";
531
628
  }
532
629
  // ─── Public API ───────────────────────────────────────────────────
533
630
  init(config) {
534
631
  if (this.initialized) {
535
- this.log("SDK already initialized \u2014 skipping");
632
+ this.debugLog("SDK already initialized \u2014 skipping");
536
633
  return;
537
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
+ }
538
642
  this.config = {
539
643
  autoCapture: true,
540
644
  debug: false,
541
645
  flushInterval: 5e3,
542
646
  maxQueueSize: 100,
543
- ...config
647
+ ...config,
648
+ apiKey: config.apiKey ?? ""
649
+ // resolved below if using identifiers
544
650
  };
545
- this.client = new PulseBoardClient(this.config.host, this.config.debug);
651
+ this.client = new PulseBoardClient(this.PULSEBOARD_API, this.config.debug);
546
652
  this.queue = new EventQueue(this.config.maxQueueSize);
547
- this.contextCollector = new ContextCollector(this.config.app ?? {});
653
+ this.contextCollector = new ContextCollector({
654
+ environment: config.environment
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() {
548
684
  this.flushTimer = setInterval(
549
685
  () => this.flush(),
550
686
  this.config.flushInterval
@@ -556,7 +692,7 @@ var PulseBoardSDK = class {
556
692
  this.autoCapture.attach();
557
693
  }
558
694
  this.initialized = true;
559
- this.log(`Initialized \u2014 host: ${this.config.host}`);
695
+ this.debugLog(`Initialized \u2014 host: ${this.PULSEBOARD_API}`);
560
696
  }
561
697
  async getContext() {
562
698
  this.assertInitialized("getContext");
@@ -565,7 +701,7 @@ var PulseBoardSDK = class {
565
701
  identify(user) {
566
702
  this.assertInitialized("identify");
567
703
  this.contextCollector.identify(user);
568
- this.log(`User identified: ${JSON.stringify(user)}`);
704
+ this.debugLog(`User identified: ${JSON.stringify(user)}`);
569
705
  }
570
706
  clearUser() {
571
707
  this.assertInitialized("clearUser");
@@ -597,26 +733,87 @@ var PulseBoardSDK = class {
597
733
  ...options.payload
598
734
  });
599
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
+ }
600
799
  // ─── Analytics API ────────────────────────────────────────────────
601
800
  startSession() {
602
801
  this.assertInitialized("startSession");
603
802
  this.contextCollector.collect().then((context) => {
604
803
  this.client.sendAnalytics("session", {
605
- apiKey: this.config.apiKey,
804
+ apiKey: this.apiKey,
606
805
  sessionId: context.session.sessionId,
607
806
  startedAt: context.session.startedAt,
608
807
  context
609
808
  });
610
- this.log(`Session started: ${context.session.sessionId}`);
611
- }).catch((err) => {
612
- this.log(`Failed to start session: ${err}`);
613
- });
809
+ this.debugLog(`Session started: ${context.session.sessionId}`);
810
+ }).catch((err) => this.debugLog(`Failed to start session: ${err}`));
614
811
  }
615
812
  endSession(duration) {
616
813
  this.assertInitialized("endSession");
617
814
  this.contextCollector.collect().then((context) => {
618
815
  this.client.sendAnalytics("session", {
619
- apiKey: this.config.apiKey,
816
+ apiKey: this.apiKey,
620
817
  sessionId: context.session.sessionId,
621
818
  startedAt: context.session.startedAt,
622
819
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -624,31 +821,27 @@ var PulseBoardSDK = class {
624
821
  context
625
822
  });
626
823
  this.flush();
627
- this.log(`Session ended: ${context.session.sessionId}`);
628
- }).catch((err) => {
629
- this.log(`Failed to end session: ${err}`);
630
- });
824
+ this.debugLog(`Session ended: ${context.session.sessionId}`);
825
+ }).catch((err) => this.debugLog(`Failed to end session: ${err}`));
631
826
  }
632
827
  trackScreen(screenName, loadTime) {
633
828
  this.assertInitialized("trackScreen");
634
829
  this.contextCollector.collect().then((context) => {
635
830
  this.client.sendAnalytics("screen-view", {
636
- apiKey: this.config.apiKey,
831
+ apiKey: this.apiKey,
637
832
  screenName,
638
833
  loadTime,
639
834
  sessionId: context.session.sessionId,
640
835
  context
641
836
  });
642
- this.log(`Screen tracked: ${screenName}`);
643
- }).catch((err) => {
644
- this.log(`Failed to track screen: ${err}`);
645
- });
837
+ this.debugLog(`Screen tracked: ${screenName}`);
838
+ }).catch((err) => this.debugLog(`Failed to track screen: ${err}`));
646
839
  }
647
840
  trackApiCall(endpoint, httpMethod, statusCode, duration, payloadSize) {
648
841
  this.assertInitialized("trackApiCall");
649
842
  this.contextCollector.collect().then((context) => {
650
843
  this.client.sendAnalytics("api-call", {
651
- apiKey: this.config.apiKey,
844
+ apiKey: this.apiKey,
652
845
  endpoint,
653
846
  httpMethod,
654
847
  statusCode,
@@ -657,16 +850,16 @@ var PulseBoardSDK = class {
657
850
  sessionId: context.session.sessionId,
658
851
  context
659
852
  });
660
- this.log(`API call tracked: ${httpMethod} ${endpoint} ${statusCode}`);
661
- }).catch((err) => {
662
- this.log(`Failed to track API call: ${err}`);
663
- });
853
+ this.debugLog(
854
+ `API call tracked: ${httpMethod} ${endpoint} ${statusCode}`
855
+ );
856
+ }).catch((err) => this.debugLog(`Failed to track API call: ${err}`));
664
857
  }
665
858
  trackCrash(error, isFatal = false) {
666
859
  this.assertInitialized("trackCrash");
667
860
  this.contextCollector.collect().then((context) => {
668
861
  this.client.sendAnalytics("crash", {
669
- apiKey: this.config.apiKey,
862
+ apiKey: this.apiKey,
670
863
  errorName: error.name,
671
864
  errorMessage: error.message,
672
865
  stackTrace: error.stack ?? "",
@@ -674,17 +867,46 @@ var PulseBoardSDK = class {
674
867
  sessionId: context.session.sessionId,
675
868
  context
676
869
  });
677
- this.log(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
678
- }).catch((err) => {
679
- this.log(`Failed to track crash: ${err}`);
680
- });
870
+ this.debugLog(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
871
+ }).catch((err) => this.debugLog(`Failed to track crash: ${err}`));
681
872
  }
682
- // ─── 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 ──────────────────────────────────────────────
683
891
  async flush() {
684
- if (!this.queue || this.queue.isEmpty) return;
685
- 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;
686
908
  const events = this.queue.dequeue(10);
687
- this.log(`Flushing ${events.length} event(s)`);
909
+ this.debugLog(`Flushing ${events.length} event(s)`);
688
910
  await this.client.sendBatch(events);
689
911
  }
690
912
  destroy() {
@@ -692,20 +914,22 @@ var PulseBoardSDK = class {
692
914
  clearInterval(this.flushTimer);
693
915
  this.flushTimer = null;
694
916
  }
917
+ this.releaseConsole();
695
918
  this.autoCapture?.detach();
696
919
  this.queue?.clear();
920
+ this.logQueue = [];
697
921
  this.initialized = false;
698
922
  this.config = null;
699
923
  this.client = null;
700
924
  this.queue = null;
701
925
  this.contextCollector = null;
702
- this.log("SDK destroyed");
926
+ this.debugLog("SDK destroyed");
703
927
  }
704
- // ─── Private ─────────────────────────────────────────────────────
928
+ // ─── Private ──────────────────────────────────────────────────────
705
929
  buildAndEnqueue(type, name, payload, timestamp) {
706
930
  this.contextCollector.collect().then((context) => {
707
931
  const event = {
708
- apiKey: this.config.apiKey,
932
+ apiKey: this.apiKey,
709
933
  type,
710
934
  name,
711
935
  payload,
@@ -713,20 +937,16 @@ var PulseBoardSDK = class {
713
937
  context
714
938
  };
715
939
  this.queue.enqueue(event);
716
- this.log(`Queued: ${type} \u2014 ${name}`);
717
- if (type === "error") {
718
- this.flush();
719
- }
720
- }).catch((err) => {
721
- this.log(`Failed to collect context: ${err}`);
722
- });
940
+ this.debugLog(`Queued: ${type} \u2014 ${name}`);
941
+ if (type === "error") this.flush();
942
+ }).catch((err) => this.debugLog(`Failed to collect context: ${err}`));
723
943
  }
724
944
  assertInitialized(method) {
725
945
  if (!this.initialized) {
726
946
  throw new Error(`PulseBoard.${method}() called before PulseBoard.init()`);
727
947
  }
728
948
  }
729
- log(message) {
949
+ debugLog(message) {
730
950
  if (this.config?.debug) {
731
951
  console.log(`[PulseBoard] ${message}`);
732
952
  }