@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.js CHANGED
@@ -1,6 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var reactNative = require('react-native');
4
+ var AsyncStorage = require('@react-native-async-storage/async-storage');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var AsyncStorage__default = /*#__PURE__*/_interopDefault(AsyncStorage);
4
9
 
5
10
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
11
  var __commonJS = (cb, mod) => function __require() {
@@ -287,6 +292,42 @@ var require_rejection_tracking = __commonJS({
287
292
  }
288
293
  });
289
294
 
295
+ // src/auto-capture.ts
296
+ var AutoCapture = class {
297
+ constructor(handler) {
298
+ this.attached = false;
299
+ this.previousHandler = null;
300
+ this.handler = handler;
301
+ }
302
+ attach() {
303
+ if (this.attached) return;
304
+ this.previousHandler = ErrorUtils.getGlobalHandler();
305
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
306
+ this.handler("uncaughtException", error, { isFatal: isFatal ?? false });
307
+ if (this.previousHandler) {
308
+ this.previousHandler(error, isFatal);
309
+ }
310
+ });
311
+ const tracking = require_rejection_tracking();
312
+ tracking.enable({
313
+ allRejections: true,
314
+ onUnhandled: (_id, error) => {
315
+ const err = error instanceof Error ? error : new Error(String(error));
316
+ this.handler("unhandledRejection", err, { fatal: false });
317
+ }
318
+ });
319
+ this.attached = true;
320
+ }
321
+ detach() {
322
+ if (!this.attached) return;
323
+ if (this.previousHandler) {
324
+ ErrorUtils.setGlobalHandler(this.previousHandler);
325
+ this.previousHandler = null;
326
+ }
327
+ this.attached = false;
328
+ }
329
+ };
330
+
290
331
  // src/client.ts
291
332
  var PulseBoardClient = class {
292
333
  constructor(host, debug = false) {
@@ -342,6 +383,60 @@ var PulseBoardClient = class {
342
383
  return false;
343
384
  }
344
385
  }
386
+ // ─── Log Entry ─────────────────────────────────────────────
387
+ async sendLog(entry) {
388
+ try {
389
+ const response = await fetch(`${this.host}/logs`, {
390
+ method: "POST",
391
+ headers: { "Content-Type": "application/json" },
392
+ body: JSON.stringify(entry)
393
+ });
394
+ if (!response.ok) {
395
+ this.log(`Failed to send log: ${response.status}`);
396
+ return false;
397
+ }
398
+ return true;
399
+ } catch (err) {
400
+ this.log(`Network error sending log: ${err}`);
401
+ return false;
402
+ }
403
+ }
404
+ async sendLogBatch(apiKey, logs) {
405
+ try {
406
+ const response = await fetch(`${this.host}/logs/batch`, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify({ apiKey, logs })
410
+ });
411
+ if (!response.ok) {
412
+ this.log(`Failed to send log batch: ${response.status}`);
413
+ return false;
414
+ }
415
+ return true;
416
+ } catch (err) {
417
+ this.log(`Network error sending log batch: ${err}`);
418
+ return false;
419
+ }
420
+ }
421
+ // ─── Feedback ─────────────────────────────────────────────────────
422
+ async sendFeedback(payload) {
423
+ try {
424
+ const response = await fetch(`${this.host}/feedback`, {
425
+ method: "POST",
426
+ headers: { "Content-Type": "application/json" },
427
+ body: JSON.stringify(payload)
428
+ });
429
+ if (!response.ok) {
430
+ this.log(`Failed to send feedback: ${response.status}`);
431
+ return false;
432
+ }
433
+ this.log("Feedback sent");
434
+ return true;
435
+ } catch (err) {
436
+ this.log(`Network error sending feedback: ${err}`);
437
+ return false;
438
+ }
439
+ }
345
440
  // ─── Private ──────────────────────────────────────────────────────
346
441
  chunk(arr, size) {
347
442
  return Array.from(
@@ -355,68 +450,6 @@ var PulseBoardClient = class {
355
450
  }
356
451
  }
357
452
  };
358
-
359
- // src/queue.ts
360
- var EventQueue = class {
361
- constructor(maxSize = 100) {
362
- this.queue = [];
363
- this.maxSize = maxSize;
364
- }
365
- enqueue(event) {
366
- if (this.queue.length >= this.maxSize) {
367
- this.queue.shift();
368
- }
369
- this.queue.push(event);
370
- }
371
- dequeue(count) {
372
- return this.queue.splice(0, count);
373
- }
374
- get size() {
375
- return this.queue.length;
376
- }
377
- get isEmpty() {
378
- return this.queue.length === 0;
379
- }
380
- clear() {
381
- this.queue = [];
382
- }
383
- };
384
-
385
- // src/auto-capture.ts
386
- var AutoCapture = class {
387
- constructor(handler) {
388
- this.attached = false;
389
- this.previousHandler = null;
390
- this.handler = handler;
391
- }
392
- attach() {
393
- if (this.attached) return;
394
- this.previousHandler = ErrorUtils.getGlobalHandler();
395
- ErrorUtils.setGlobalHandler((error, isFatal) => {
396
- this.handler("uncaughtException", error, { isFatal: isFatal ?? false });
397
- if (this.previousHandler) {
398
- this.previousHandler(error, isFatal);
399
- }
400
- });
401
- const tracking = require_rejection_tracking();
402
- tracking.enable({
403
- allRejections: true,
404
- onUnhandled: (_id, error) => {
405
- const err = error instanceof Error ? error : new Error(String(error));
406
- this.handler("unhandledRejection", err, { fatal: false });
407
- }
408
- });
409
- this.attached = true;
410
- }
411
- detach() {
412
- if (!this.attached) return;
413
- if (this.previousHandler) {
414
- ErrorUtils.setGlobalHandler(this.previousHandler);
415
- this.previousHandler = null;
416
- }
417
- this.attached = false;
418
- }
419
- };
420
453
  var NativePulseBoardDevice_default = reactNative.TurboModuleRegistry.getEnforcing("PulseBoardDevice");
421
454
  var NativePulseBoardNetwork_default = reactNative.TurboModuleRegistry.getEnforcing("PulseBoardNetwork");
422
455
 
@@ -493,8 +526,8 @@ var ContextCollector = class {
493
526
  manufacturer: deviceInfo?.manufacturer ?? "unknown",
494
527
  brand: deviceInfo?.brand ?? "unknown",
495
528
  isTablet: deviceInfo?.isTablet ?? false,
496
- appVersion: this.appContext.appVersion ?? deviceInfo?.appVersion ?? "unknown",
497
- buildNumber: this.appContext.buildNumber ?? deviceInfo?.buildNumber ?? "unknown",
529
+ appVersion: deviceInfo?.appVersion ?? "unknown",
530
+ buildNumber: deviceInfo?.buildNumber ?? "unknown",
498
531
  bundleId: deviceInfo?.bundleId ?? "unknown",
499
532
  screenWidth: deviceInfo?.screenWidth ?? width,
500
533
  screenHeight: deviceInfo?.screenHeight ?? height,
@@ -519,6 +552,65 @@ var ContextCollector = class {
519
552
  };
520
553
  }
521
554
  };
555
+ var QUEUE_KEY = "@pulseboard/event_queue";
556
+ var MAX_QUEUE_SIZE = 100;
557
+ var EventQueue = class {
558
+ constructor(maxSize = MAX_QUEUE_SIZE) {
559
+ this.memoryQueue = [];
560
+ this._hydrated = false;
561
+ this.maxSize = maxSize;
562
+ this.hydrate();
563
+ }
564
+ // ─── Hydrate from storage on init ─────────────────────────────────
565
+ async hydrate() {
566
+ try {
567
+ const raw = await AsyncStorage__default.default.getItem(QUEUE_KEY);
568
+ if (raw) {
569
+ const stored = JSON.parse(raw);
570
+ this.memoryQueue = stored.slice(0, this.maxSize);
571
+ }
572
+ } catch {
573
+ } finally {
574
+ this._hydrated = true;
575
+ }
576
+ }
577
+ // ─── Persist to storage ────────────────────────────────────────────
578
+ async persist() {
579
+ try {
580
+ await AsyncStorage__default.default.setItem(QUEUE_KEY, JSON.stringify(this.memoryQueue));
581
+ } catch {
582
+ }
583
+ }
584
+ // ─── Public API ────────────────────────────────────────────────────
585
+ enqueue(event) {
586
+ if (this.memoryQueue.length >= this.maxSize) {
587
+ this.memoryQueue.shift();
588
+ }
589
+ this.memoryQueue.push(event);
590
+ this.persist();
591
+ }
592
+ dequeue(count) {
593
+ const batch = this.memoryQueue.splice(0, count);
594
+ this.persist();
595
+ return batch;
596
+ }
597
+ get size() {
598
+ return this.memoryQueue.length;
599
+ }
600
+ get isEmpty() {
601
+ return this.memoryQueue.length === 0;
602
+ }
603
+ get hydrated() {
604
+ return this._hydrated;
605
+ }
606
+ async clear() {
607
+ this.memoryQueue = [];
608
+ try {
609
+ await AsyncStorage__default.default.removeItem(QUEUE_KEY);
610
+ } catch {
611
+ }
612
+ }
613
+ };
522
614
 
523
615
  // src/index.ts
524
616
  var PulseBoardSDK = class {
@@ -530,23 +622,71 @@ var PulseBoardSDK = class {
530
622
  this.contextCollector = null;
531
623
  this.flushTimer = null;
532
624
  this.initialized = false;
625
+ this.PULSEBOARD_API = "https://pulseboard-production.up.railway.app/";
626
+ // ─── Log queue ────────────────────────────────────────────────────
627
+ this.logQueue = [];
628
+ this.logFlushTimer = null;
629
+ this.originalConsole = {};
630
+ this.consoleCapturing = false;
631
+ }
632
+ get apiKey() {
633
+ return this.config?.apiKey ?? "";
533
634
  }
534
635
  // ─── Public API ───────────────────────────────────────────────────
535
636
  init(config) {
536
637
  if (this.initialized) {
537
- this.log("SDK already initialized \u2014 skipping");
638
+ this.debugLog("SDK already initialized \u2014 skipping");
538
639
  return;
539
640
  }
641
+ const hasDirectKey = !!config.apiKey;
642
+ const hasIdentifiers = !!(config.organisation && config.product && config.project);
643
+ if (!hasDirectKey && !hasIdentifiers) {
644
+ throw new Error(
645
+ "[PulseBoard] init() requires either apiKey or (organisation + product + project)"
646
+ );
647
+ }
540
648
  this.config = {
541
649
  autoCapture: true,
542
650
  debug: false,
543
651
  flushInterval: 5e3,
544
652
  maxQueueSize: 100,
545
- ...config
653
+ ...config,
654
+ apiKey: config.apiKey ?? ""
655
+ // resolved below if using identifiers
546
656
  };
547
- this.client = new PulseBoardClient(this.config.host, this.config.debug);
657
+ this.client = new PulseBoardClient(this.PULSEBOARD_API, this.config.debug);
548
658
  this.queue = new EventQueue(this.config.maxQueueSize);
549
- this.contextCollector = new ContextCollector(this.config.app ?? {});
659
+ this.contextCollector = new ContextCollector({
660
+ environment: config.environment
661
+ });
662
+ if (!hasDirectKey && hasIdentifiers) {
663
+ this.resolveApiKey(
664
+ config.organisation,
665
+ config.product,
666
+ config.project
667
+ );
668
+ } else {
669
+ this.completeInit();
670
+ }
671
+ }
672
+ async resolveApiKey(organisation, product, project) {
673
+ try {
674
+ const response = await fetch(
675
+ `${this.PULSEBOARD_API}sdk/resolve?organisation=${encodeURIComponent(organisation)}&product=${encodeURIComponent(product)}&project=${encodeURIComponent(project)}`
676
+ );
677
+ if (!response.ok) {
678
+ throw new Error(
679
+ `[PulseBoard] Could not resolve project. Check your organisation, product and project values.`
680
+ );
681
+ }
682
+ const data = await response.json();
683
+ this.config.apiKey = data.data.apiKey;
684
+ this.completeInit();
685
+ } catch (err) {
686
+ console.error("[PulseBoard] Failed to resolve API key:", err);
687
+ }
688
+ }
689
+ completeInit() {
550
690
  this.flushTimer = setInterval(
551
691
  () => this.flush(),
552
692
  this.config.flushInterval
@@ -558,7 +698,7 @@ var PulseBoardSDK = class {
558
698
  this.autoCapture.attach();
559
699
  }
560
700
  this.initialized = true;
561
- this.log(`Initialized \u2014 host: ${this.config.host}`);
701
+ this.debugLog(`Initialized \u2014 host: ${this.PULSEBOARD_API}`);
562
702
  }
563
703
  async getContext() {
564
704
  this.assertInitialized("getContext");
@@ -567,7 +707,7 @@ var PulseBoardSDK = class {
567
707
  identify(user) {
568
708
  this.assertInitialized("identify");
569
709
  this.contextCollector.identify(user);
570
- this.log(`User identified: ${JSON.stringify(user)}`);
710
+ this.debugLog(`User identified: ${JSON.stringify(user)}`);
571
711
  }
572
712
  clearUser() {
573
713
  this.assertInitialized("clearUser");
@@ -599,26 +739,87 @@ var PulseBoardSDK = class {
599
739
  ...options.payload
600
740
  });
601
741
  }
742
+ // ─── Logging API ──────────────────────────────────────────────────
743
+ log(level, message, options = {}) {
744
+ this.assertInitialized("log");
745
+ this.contextCollector.collect().then((context) => {
746
+ this.logQueue.push({
747
+ level,
748
+ message,
749
+ meta: options.meta,
750
+ sessionId: context.session.sessionId,
751
+ appVersion: context.device.appVersion,
752
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
753
+ });
754
+ if (level === "error") this.flushLogs();
755
+ }).catch(() => {
756
+ });
757
+ }
758
+ captureConsole() {
759
+ this.assertInitialized("captureConsole");
760
+ if (this.consoleCapturing) return;
761
+ this.originalConsole = {
762
+ log: console.log,
763
+ warn: console.warn,
764
+ error: console.error,
765
+ debug: console.debug
766
+ };
767
+ console.log = (...args) => {
768
+ this.originalConsole.log?.(...args);
769
+ this.log("info", args.map(String).join(" "));
770
+ };
771
+ console.warn = (...args) => {
772
+ this.originalConsole.warn?.(...args);
773
+ this.log("warn", args.map(String).join(" "));
774
+ };
775
+ console.error = (...args) => {
776
+ this.originalConsole.error?.(...args);
777
+ this.log("error", args.map(String).join(" "));
778
+ };
779
+ console.debug = (...args) => {
780
+ this.originalConsole.debug?.(...args);
781
+ this.log("debug", args.map(String).join(" "));
782
+ };
783
+ this.logFlushTimer = setInterval(() => this.flushLogs(), 1e4);
784
+ this.consoleCapturing = true;
785
+ this.debugLog("Console capture enabled");
786
+ }
787
+ releaseConsole() {
788
+ if (!this.consoleCapturing) return;
789
+ if (this.originalConsole.log) console.log = this.originalConsole.log;
790
+ if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
791
+ if (this.originalConsole.error) console.error = this.originalConsole.error;
792
+ if (this.originalConsole.debug) console.debug = this.originalConsole.debug;
793
+ if (this.logFlushTimer) {
794
+ clearInterval(this.logFlushTimer);
795
+ this.logFlushTimer = null;
796
+ }
797
+ this.originalConsole = {};
798
+ this.consoleCapturing = false;
799
+ }
800
+ async flushLogs() {
801
+ if (!this.client || !this.config || this.logQueue.length === 0) return;
802
+ const batch = this.logQueue.splice(0, this.logQueue.length);
803
+ await this.client.sendLogBatch(this.apiKey, batch);
804
+ }
602
805
  // ─── Analytics API ────────────────────────────────────────────────
603
806
  startSession() {
604
807
  this.assertInitialized("startSession");
605
808
  this.contextCollector.collect().then((context) => {
606
809
  this.client.sendAnalytics("session", {
607
- apiKey: this.config.apiKey,
810
+ apiKey: this.apiKey,
608
811
  sessionId: context.session.sessionId,
609
812
  startedAt: context.session.startedAt,
610
813
  context
611
814
  });
612
- this.log(`Session started: ${context.session.sessionId}`);
613
- }).catch((err) => {
614
- this.log(`Failed to start session: ${err}`);
615
- });
815
+ this.debugLog(`Session started: ${context.session.sessionId}`);
816
+ }).catch((err) => this.debugLog(`Failed to start session: ${err}`));
616
817
  }
617
818
  endSession(duration) {
618
819
  this.assertInitialized("endSession");
619
820
  this.contextCollector.collect().then((context) => {
620
821
  this.client.sendAnalytics("session", {
621
- apiKey: this.config.apiKey,
822
+ apiKey: this.apiKey,
622
823
  sessionId: context.session.sessionId,
623
824
  startedAt: context.session.startedAt,
624
825
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -626,31 +827,27 @@ var PulseBoardSDK = class {
626
827
  context
627
828
  });
628
829
  this.flush();
629
- this.log(`Session ended: ${context.session.sessionId}`);
630
- }).catch((err) => {
631
- this.log(`Failed to end session: ${err}`);
632
- });
830
+ this.debugLog(`Session ended: ${context.session.sessionId}`);
831
+ }).catch((err) => this.debugLog(`Failed to end session: ${err}`));
633
832
  }
634
833
  trackScreen(screenName, loadTime) {
635
834
  this.assertInitialized("trackScreen");
636
835
  this.contextCollector.collect().then((context) => {
637
836
  this.client.sendAnalytics("screen-view", {
638
- apiKey: this.config.apiKey,
837
+ apiKey: this.apiKey,
639
838
  screenName,
640
839
  loadTime,
641
840
  sessionId: context.session.sessionId,
642
841
  context
643
842
  });
644
- this.log(`Screen tracked: ${screenName}`);
645
- }).catch((err) => {
646
- this.log(`Failed to track screen: ${err}`);
647
- });
843
+ this.debugLog(`Screen tracked: ${screenName}`);
844
+ }).catch((err) => this.debugLog(`Failed to track screen: ${err}`));
648
845
  }
649
846
  trackApiCall(endpoint, httpMethod, statusCode, duration, payloadSize) {
650
847
  this.assertInitialized("trackApiCall");
651
848
  this.contextCollector.collect().then((context) => {
652
849
  this.client.sendAnalytics("api-call", {
653
- apiKey: this.config.apiKey,
850
+ apiKey: this.apiKey,
654
851
  endpoint,
655
852
  httpMethod,
656
853
  statusCode,
@@ -659,16 +856,16 @@ var PulseBoardSDK = class {
659
856
  sessionId: context.session.sessionId,
660
857
  context
661
858
  });
662
- this.log(`API call tracked: ${httpMethod} ${endpoint} ${statusCode}`);
663
- }).catch((err) => {
664
- this.log(`Failed to track API call: ${err}`);
665
- });
859
+ this.debugLog(
860
+ `API call tracked: ${httpMethod} ${endpoint} ${statusCode}`
861
+ );
862
+ }).catch((err) => this.debugLog(`Failed to track API call: ${err}`));
666
863
  }
667
864
  trackCrash(error, isFatal = false) {
668
865
  this.assertInitialized("trackCrash");
669
866
  this.contextCollector.collect().then((context) => {
670
867
  this.client.sendAnalytics("crash", {
671
- apiKey: this.config.apiKey,
868
+ apiKey: this.apiKey,
672
869
  errorName: error.name,
673
870
  errorMessage: error.message,
674
871
  stackTrace: error.stack ?? "",
@@ -676,17 +873,46 @@ var PulseBoardSDK = class {
676
873
  sessionId: context.session.sessionId,
677
874
  context
678
875
  });
679
- this.log(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
680
- }).catch((err) => {
681
- this.log(`Failed to track crash: ${err}`);
682
- });
876
+ this.debugLog(`Crash tracked: ${error.name} \u2014 fatal: ${isFatal}`);
877
+ }).catch((err) => this.debugLog(`Failed to track crash: ${err}`));
683
878
  }
684
- // ─── Flush & Destroy ─────────────────────────────────────────────
879
+ feedback(message, options = {}) {
880
+ this.assertInitialized("feedback");
881
+ this.contextCollector.collect().then((context) => {
882
+ this.client.sendFeedback({
883
+ apiKey: this.apiKey,
884
+ type: options.type ?? "general",
885
+ message,
886
+ meta: options.meta,
887
+ userEmail: options.userEmail,
888
+ userName: options.userName,
889
+ screenshot: options.screenshot,
890
+ sessionId: context.session.sessionId,
891
+ appVersion: context.device.appVersion
892
+ });
893
+ this.debugLog(`Feedback sent: ${options.type ?? "general"}`);
894
+ }).catch((err) => this.debugLog(`Failed to send feedback: ${err}`));
895
+ }
896
+ // ─── Flush & Destroy ──────────────────────────────────────────────
685
897
  async flush() {
686
- if (!this.queue || this.queue.isEmpty) return;
687
- if (!this.client) return;
898
+ if (!this.queue || !this.client) return;
899
+ if (!this.queue.hydrated) {
900
+ await new Promise((resolve) => {
901
+ const interval = setInterval(() => {
902
+ if (this.queue?.hydrated) {
903
+ clearInterval(interval);
904
+ resolve();
905
+ }
906
+ }, 50);
907
+ setTimeout(() => {
908
+ clearInterval(interval);
909
+ resolve();
910
+ }, 500);
911
+ });
912
+ }
913
+ if (this.queue.isEmpty) return;
688
914
  const events = this.queue.dequeue(10);
689
- this.log(`Flushing ${events.length} event(s)`);
915
+ this.debugLog(`Flushing ${events.length} event(s)`);
690
916
  await this.client.sendBatch(events);
691
917
  }
692
918
  destroy() {
@@ -694,20 +920,22 @@ var PulseBoardSDK = class {
694
920
  clearInterval(this.flushTimer);
695
921
  this.flushTimer = null;
696
922
  }
923
+ this.releaseConsole();
697
924
  this.autoCapture?.detach();
698
925
  this.queue?.clear();
926
+ this.logQueue = [];
699
927
  this.initialized = false;
700
928
  this.config = null;
701
929
  this.client = null;
702
930
  this.queue = null;
703
931
  this.contextCollector = null;
704
- this.log("SDK destroyed");
932
+ this.debugLog("SDK destroyed");
705
933
  }
706
- // ─── Private ─────────────────────────────────────────────────────
934
+ // ─── Private ──────────────────────────────────────────────────────
707
935
  buildAndEnqueue(type, name, payload, timestamp) {
708
936
  this.contextCollector.collect().then((context) => {
709
937
  const event = {
710
- apiKey: this.config.apiKey,
938
+ apiKey: this.apiKey,
711
939
  type,
712
940
  name,
713
941
  payload,
@@ -715,20 +943,16 @@ var PulseBoardSDK = class {
715
943
  context
716
944
  };
717
945
  this.queue.enqueue(event);
718
- this.log(`Queued: ${type} \u2014 ${name}`);
719
- if (type === "error") {
720
- this.flush();
721
- }
722
- }).catch((err) => {
723
- this.log(`Failed to collect context: ${err}`);
724
- });
946
+ this.debugLog(`Queued: ${type} \u2014 ${name}`);
947
+ if (type === "error") this.flush();
948
+ }).catch((err) => this.debugLog(`Failed to collect context: ${err}`));
725
949
  }
726
950
  assertInitialized(method) {
727
951
  if (!this.initialized) {
728
952
  throw new Error(`PulseBoard.${method}() called before PulseBoard.init()`);
729
953
  }
730
954
  }
731
- log(message) {
955
+ debugLog(message) {
732
956
  if (this.config?.debug) {
733
957
  console.log(`[PulseBoard] ${message}`);
734
958
  }