@scalebun/react-native 1.0.3 → 1.0.4

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.
Files changed (223) hide show
  1. package/android/src/main/java/com/scalebun/core/crash/CrashStorage.kt +115 -1
  2. package/android/src/main/java/com/scalebun/core/crash/NativeCrashHandler.kt +14 -0
  3. package/android/src/main/java/com/scalebun/replaysdk/ReplaySdkModule.kt +25 -9
  4. package/android/src/main/java/com/scalebun/replaysdk/voice/VoiceTranscriber.kt +68 -2
  5. package/android/src/main/java/com/scalebun/rn/crash/ScaleBunCrashModule.kt +66 -2
  6. package/ios/Crash/ScaleBunCrashBridge.mm +6 -0
  7. package/ios/Crash/ScaleBunCrashHandler.swift +14 -1
  8. package/ios/Crash/ScaleBunCrashModule.swift +14 -0
  9. package/ios/Crash/ScaleBunCrashSignalBridge.h +32 -1
  10. package/ios/Crash/ScaleBunCrashSignalBridge.m +172 -7
  11. package/ios/Crash/ScaleBunCrashSignalStress.c +137 -0
  12. package/ios/Crash/ScaleBunCrashStorage.swift +75 -0
  13. package/lib/commonjs/analytics/EventTracker.js +30 -2
  14. package/lib/commonjs/analytics/EventTracker.js.map +1 -1
  15. package/lib/commonjs/bootstrap/SDKBootstrapper.js +19 -14
  16. package/lib/commonjs/bootstrap/SDKBootstrapper.js.map +1 -1
  17. package/lib/commonjs/core/config/schema.js +13 -5
  18. package/lib/commonjs/core/config/schema.js.map +1 -1
  19. package/lib/commonjs/core/context/device.js +130 -0
  20. package/lib/commonjs/core/context/device.js.map +1 -1
  21. package/lib/commonjs/debug/replayWiring.js +2 -1
  22. package/lib/commonjs/debug/replayWiring.js.map +1 -1
  23. package/lib/commonjs/features/bugreport/BugReportFeature.js +112 -0
  24. package/lib/commonjs/features/bugreport/BugReportFeature.js.map +1 -0
  25. package/lib/commonjs/features/bugreport/buildPayload.js +97 -0
  26. package/lib/commonjs/features/bugreport/buildPayload.js.map +1 -0
  27. package/lib/commonjs/features/crash/CrashReporter.js +25 -1
  28. package/lib/commonjs/features/crash/CrashReporter.js.map +1 -1
  29. package/lib/commonjs/features/crash/capture.js +27 -9
  30. package/lib/commonjs/features/crash/capture.js.map +1 -1
  31. package/lib/commonjs/features/crash/nativeCrashBridge.js +163 -28
  32. package/lib/commonjs/features/crash/nativeCrashBridge.js.map +1 -1
  33. package/lib/commonjs/features/engage/EngagePromptProvider.js +36 -2
  34. package/lib/commonjs/features/engage/EngagePromptProvider.js.map +1 -1
  35. package/lib/commonjs/features/engage/EngagePromptView.js +47 -14
  36. package/lib/commonjs/features/engage/EngagePromptView.js.map +1 -1
  37. package/lib/commonjs/features/engage/engageTriggerEngine.js +10 -0
  38. package/lib/commonjs/features/engage/engageTriggerEngine.js.map +1 -1
  39. package/lib/commonjs/features/engage/engageTypes.js.map +1 -1
  40. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +2 -1
  41. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js.map +1 -1
  42. package/lib/commonjs/features/ota/OtaOrchestrator.js +63 -5
  43. package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
  44. package/lib/commonjs/features/ota/deviceAttributes.js +48 -0
  45. package/lib/commonjs/features/ota/deviceAttributes.js.map +1 -0
  46. package/lib/commonjs/features/ota/geoCountry.js +93 -0
  47. package/lib/commonjs/features/ota/geoCountry.js.map +1 -0
  48. package/lib/commonjs/features/ota/useOtaUpdate.js +4 -1
  49. package/lib/commonjs/features/ota/useOtaUpdate.js.map +1 -1
  50. package/lib/commonjs/features/performance/PerformanceFeature.js +23 -9
  51. package/lib/commonjs/features/performance/PerformanceFeature.js.map +1 -1
  52. package/lib/commonjs/features/performance/collectors/FrameMetricsCollector.js +69 -9
  53. package/lib/commonjs/features/performance/collectors/FrameMetricsCollector.js.map +1 -1
  54. package/lib/commonjs/features/performance/transport/PerformanceTransport.js +25 -0
  55. package/lib/commonjs/features/performance/transport/PerformanceTransport.js.map +1 -1
  56. package/lib/commonjs/features/session/BackendSessionAdapter.js +128 -11
  57. package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
  58. package/lib/commonjs/features/session/JourneyEventPipeline.js +1 -0
  59. package/lib/commonjs/features/session/JourneyEventPipeline.js.map +1 -1
  60. package/lib/commonjs/features/session/ReplayCaptureManager.js +46 -0
  61. package/lib/commonjs/features/session/ReplayCaptureManager.js.map +1 -1
  62. package/lib/commonjs/features/session/SessionManager.js +42 -12
  63. package/lib/commonjs/features/session/SessionManager.js.map +1 -1
  64. package/lib/commonjs/features/session/nativeCapture.js +4 -4
  65. package/lib/commonjs/features/session/nativeCapture.js.map +1 -1
  66. package/lib/commonjs/features/session/outboxFrameCapture.js +3 -2
  67. package/lib/commonjs/features/session/outboxFrameCapture.js.map +1 -1
  68. package/lib/commonjs/features/session/sessionTypes.js.map +1 -1
  69. package/lib/commonjs/index.js +13 -0
  70. package/lib/commonjs/index.js.map +1 -1
  71. package/lib/commonjs/public/ScaleBunFacade.js +150 -2
  72. package/lib/commonjs/public/ScaleBunFacade.js.map +1 -1
  73. package/lib/commonjs/specs/NativeScaleBunCrash.js.map +1 -1
  74. package/lib/module/analytics/EventTracker.js +29 -1
  75. package/lib/module/analytics/EventTracker.js.map +1 -1
  76. package/lib/module/bootstrap/SDKBootstrapper.js +17 -12
  77. package/lib/module/bootstrap/SDKBootstrapper.js.map +1 -1
  78. package/lib/module/core/config/schema.js +13 -5
  79. package/lib/module/core/config/schema.js.map +1 -1
  80. package/lib/module/core/context/device.js +119 -0
  81. package/lib/module/core/context/device.js.map +1 -1
  82. package/lib/module/debug/replayWiring.js +2 -1
  83. package/lib/module/debug/replayWiring.js.map +1 -1
  84. package/lib/module/features/bugreport/BugReportFeature.js +105 -0
  85. package/lib/module/features/bugreport/BugReportFeature.js.map +1 -0
  86. package/lib/module/features/bugreport/buildPayload.js +91 -0
  87. package/lib/module/features/bugreport/buildPayload.js.map +1 -0
  88. package/lib/module/features/crash/CrashReporter.js +25 -1
  89. package/lib/module/features/crash/CrashReporter.js.map +1 -1
  90. package/lib/module/features/crash/capture.js +28 -9
  91. package/lib/module/features/crash/capture.js.map +1 -1
  92. package/lib/module/features/crash/nativeCrashBridge.js +161 -28
  93. package/lib/module/features/crash/nativeCrashBridge.js.map +1 -1
  94. package/lib/module/features/engage/EngagePromptProvider.js +36 -2
  95. package/lib/module/features/engage/EngagePromptProvider.js.map +1 -1
  96. package/lib/module/features/engage/EngagePromptView.js +47 -14
  97. package/lib/module/features/engage/EngagePromptView.js.map +1 -1
  98. package/lib/module/features/engage/engageTriggerEngine.js +10 -0
  99. package/lib/module/features/engage/engageTriggerEngine.js.map +1 -1
  100. package/lib/module/features/engage/engageTypes.js.map +1 -1
  101. package/lib/module/features/journey/ScaleBunDebugRoot.js +2 -1
  102. package/lib/module/features/journey/ScaleBunDebugRoot.js.map +1 -1
  103. package/lib/module/features/ota/OtaOrchestrator.js +63 -5
  104. package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
  105. package/lib/module/features/ota/deviceAttributes.js +43 -0
  106. package/lib/module/features/ota/deviceAttributes.js.map +1 -0
  107. package/lib/module/features/ota/geoCountry.js +86 -0
  108. package/lib/module/features/ota/geoCountry.js.map +1 -0
  109. package/lib/module/features/ota/useOtaUpdate.js +4 -1
  110. package/lib/module/features/ota/useOtaUpdate.js.map +1 -1
  111. package/lib/module/features/performance/PerformanceFeature.js +23 -9
  112. package/lib/module/features/performance/PerformanceFeature.js.map +1 -1
  113. package/lib/module/features/performance/collectors/FrameMetricsCollector.js +70 -10
  114. package/lib/module/features/performance/collectors/FrameMetricsCollector.js.map +1 -1
  115. package/lib/module/features/performance/transport/PerformanceTransport.js +25 -0
  116. package/lib/module/features/performance/transport/PerformanceTransport.js.map +1 -1
  117. package/lib/module/features/session/BackendSessionAdapter.js +128 -11
  118. package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
  119. package/lib/module/features/session/JourneyEventPipeline.js +1 -0
  120. package/lib/module/features/session/JourneyEventPipeline.js.map +1 -1
  121. package/lib/module/features/session/ReplayCaptureManager.js +46 -0
  122. package/lib/module/features/session/ReplayCaptureManager.js.map +1 -1
  123. package/lib/module/features/session/SessionManager.js +42 -12
  124. package/lib/module/features/session/SessionManager.js.map +1 -1
  125. package/lib/module/features/session/nativeCapture.js +4 -5
  126. package/lib/module/features/session/nativeCapture.js.map +1 -1
  127. package/lib/module/features/session/outboxFrameCapture.js +3 -2
  128. package/lib/module/features/session/outboxFrameCapture.js.map +1 -1
  129. package/lib/module/features/session/sessionTypes.js.map +1 -1
  130. package/lib/module/index.js +3 -0
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/public/ScaleBunFacade.js +150 -2
  133. package/lib/module/public/ScaleBunFacade.js.map +1 -1
  134. package/lib/module/specs/NativeScaleBunCrash.js.map +1 -1
  135. package/lib/typescript/analytics/EventTracker.d.ts +20 -0
  136. package/lib/typescript/analytics/EventTracker.d.ts.map +1 -1
  137. package/lib/typescript/bootstrap/SDKBootstrapper.d.ts.map +1 -1
  138. package/lib/typescript/core/config/schema.d.ts +1 -1
  139. package/lib/typescript/core/config/schema.d.ts.map +1 -1
  140. package/lib/typescript/core/context/device.d.ts +46 -0
  141. package/lib/typescript/core/context/device.d.ts.map +1 -1
  142. package/lib/typescript/debug/replayWiring.d.ts.map +1 -1
  143. package/lib/typescript/features/bugreport/BugReportFeature.d.ts +59 -0
  144. package/lib/typescript/features/bugreport/BugReportFeature.d.ts.map +1 -0
  145. package/lib/typescript/features/bugreport/buildPayload.d.ts +51 -0
  146. package/lib/typescript/features/bugreport/buildPayload.d.ts.map +1 -0
  147. package/lib/typescript/features/crash/CrashReporter.d.ts +20 -1
  148. package/lib/typescript/features/crash/CrashReporter.d.ts.map +1 -1
  149. package/lib/typescript/features/crash/capture.d.ts +21 -2
  150. package/lib/typescript/features/crash/capture.d.ts.map +1 -1
  151. package/lib/typescript/features/crash/nativeCrashBridge.d.ts +16 -3
  152. package/lib/typescript/features/crash/nativeCrashBridge.d.ts.map +1 -1
  153. package/lib/typescript/features/engage/EngagePromptProvider.d.ts.map +1 -1
  154. package/lib/typescript/features/engage/EngagePromptView.d.ts.map +1 -1
  155. package/lib/typescript/features/engage/engageTriggerEngine.d.ts.map +1 -1
  156. package/lib/typescript/features/engage/engageTypes.d.ts +15 -0
  157. package/lib/typescript/features/engage/engageTypes.d.ts.map +1 -1
  158. package/lib/typescript/features/journey/ScaleBunDebugRoot.d.ts.map +1 -1
  159. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +8 -2
  160. package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
  161. package/lib/typescript/features/ota/deviceAttributes.d.ts +18 -0
  162. package/lib/typescript/features/ota/deviceAttributes.d.ts.map +1 -0
  163. package/lib/typescript/features/ota/geoCountry.d.ts +14 -0
  164. package/lib/typescript/features/ota/geoCountry.d.ts.map +1 -0
  165. package/lib/typescript/features/ota/useOtaUpdate.d.ts +20 -2
  166. package/lib/typescript/features/ota/useOtaUpdate.d.ts.map +1 -1
  167. package/lib/typescript/features/performance/PerformanceFeature.d.ts +0 -1
  168. package/lib/typescript/features/performance/PerformanceFeature.d.ts.map +1 -1
  169. package/lib/typescript/features/performance/collectors/FrameMetricsCollector.d.ts +33 -0
  170. package/lib/typescript/features/performance/collectors/FrameMetricsCollector.d.ts.map +1 -1
  171. package/lib/typescript/features/performance/transport/PerformanceTransport.d.ts.map +1 -1
  172. package/lib/typescript/features/session/BackendSessionAdapter.d.ts +42 -2
  173. package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
  174. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +2 -0
  175. package/lib/typescript/features/session/JourneyEventPipeline.d.ts.map +1 -1
  176. package/lib/typescript/features/session/ReplayCaptureManager.d.ts +38 -1
  177. package/lib/typescript/features/session/ReplayCaptureManager.d.ts.map +1 -1
  178. package/lib/typescript/features/session/SessionManager.d.ts.map +1 -1
  179. package/lib/typescript/features/session/nativeCapture.d.ts +2 -1
  180. package/lib/typescript/features/session/nativeCapture.d.ts.map +1 -1
  181. package/lib/typescript/features/session/outboxFrameCapture.d.ts.map +1 -1
  182. package/lib/typescript/features/session/sessionTypes.d.ts +20 -3
  183. package/lib/typescript/features/session/sessionTypes.d.ts.map +1 -1
  184. package/lib/typescript/index.d.ts +1 -0
  185. package/lib/typescript/index.d.ts.map +1 -1
  186. package/lib/typescript/public/ScaleBunFacade.d.ts +58 -0
  187. package/lib/typescript/public/ScaleBunFacade.d.ts.map +1 -1
  188. package/lib/typescript/specs/NativeScaleBunCrash.d.ts +14 -0
  189. package/lib/typescript/specs/NativeScaleBunCrash.d.ts.map +1 -1
  190. package/lib/typescript/vendor/protocol/index.d.ts +17 -17
  191. package/package.json +1 -1
  192. package/src/analytics/EventTracker.ts +28 -1
  193. package/src/bootstrap/SDKBootstrapper.ts +15 -14
  194. package/src/core/config/schema.ts +13 -5
  195. package/src/core/context/device.ts +146 -0
  196. package/src/debug/replayWiring.ts +2 -1
  197. package/src/features/bugreport/BugReportFeature.ts +128 -0
  198. package/src/features/bugreport/buildPayload.ts +116 -0
  199. package/src/features/crash/CrashReporter.ts +40 -1
  200. package/src/features/crash/capture.ts +36 -9
  201. package/src/features/crash/nativeCrashBridge.ts +156 -22
  202. package/src/features/engage/EngagePromptProvider.tsx +48 -2
  203. package/src/features/engage/EngagePromptView.tsx +51 -15
  204. package/src/features/engage/engageTriggerEngine.ts +15 -0
  205. package/src/features/engage/engageTypes.ts +15 -0
  206. package/src/features/journey/ScaleBunDebugRoot.tsx +2 -1
  207. package/src/features/ota/OtaOrchestrator.ts +75 -6
  208. package/src/features/ota/deviceAttributes.ts +46 -0
  209. package/src/features/ota/geoCountry.ts +85 -0
  210. package/src/features/ota/useOtaUpdate.ts +20 -2
  211. package/src/features/performance/PerformanceFeature.ts +22 -9
  212. package/src/features/performance/collectors/FrameMetricsCollector.ts +72 -10
  213. package/src/features/performance/transport/PerformanceTransport.ts +18 -0
  214. package/src/features/session/BackendSessionAdapter.ts +137 -10
  215. package/src/features/session/JourneyEventPipeline.ts +3 -0
  216. package/src/features/session/ReplayCaptureManager.ts +53 -1
  217. package/src/features/session/SessionManager.ts +46 -7
  218. package/src/features/session/nativeCapture.ts +3 -2
  219. package/src/features/session/outboxFrameCapture.ts +3 -2
  220. package/src/features/session/sessionTypes.ts +26 -3
  221. package/src/index.ts +3 -0
  222. package/src/public/ScaleBunFacade.ts +153 -2
  223. package/src/specs/NativeScaleBunCrash.ts +16 -0
@@ -40,6 +40,7 @@ import { generateEventId } from '../replay/core/ids/sessionId';
40
40
  import { bridgeAdapter } from '../replay/bridge/adapters/bridgeAdapter';
41
41
  import { redactBody } from '../../debug/redaction';
42
42
  import { consumeCalibrationTarget } from '../journey/calibrationContext';
43
+ import { resolvePlatformOS } from '../../core/context/device';
43
44
 
44
45
  // ─── Types ──────────────────────────────────────────────────────────────────
45
46
 
@@ -385,12 +386,11 @@ export class SessionManager {
385
386
  startedAt: Date.now(),
386
387
  status: 'active',
387
388
  metadata: {
388
- platform: (() => {
389
- try {
390
- const { Platform } = require('react-native');
391
- return Platform.OS === 'ios' ? 'ios' : 'android';
392
- } catch { return 'android'; }
393
- })(), // Real OS; enriched by native device info when available
389
+ // The real Platform.OS. This used to be a ternary whose else-branch was 'android'
390
+ // and whose catch was ALSO 'android' — so a macOS session, a Windows session, and
391
+ // a failure to load react-native at all were reported identically, as Android.
392
+ // Enriched by native device info when the bridge answers.
393
+ platform: resolvePlatformOS(),
394
394
  buildType: __DEV__ ? 'debug' : 'release',
395
395
  ...metadata,
396
396
  // Capture window dp dims here so emitEvent() can normalize coords
@@ -748,6 +748,20 @@ export class SessionManager {
748
748
  // new object and strips known-sensitive keys; coords/gesture survive.
749
749
  const safePayload = redactBody(payload) as Record<string, unknown>;
750
750
 
751
+ /**
752
+ * NO `frameId` HERE, and it is not an oversight.
753
+ *
754
+ * This method is guarded by `if (this.active) return;` — it runs ONLY when no replay
755
+ * recording is active, which is precisely when no screenshots are being captured.
756
+ * There is no frame on screen to point at, so an analytics-lane tap can never be
757
+ * attributed to a visual state.
758
+ *
759
+ * That makes it a permanent property of this lane rather than a gap to close later,
760
+ * and the dashboard has to account for it: these taps live in the heatmap grid
761
+ * alongside recorded ones, so a state filter necessarily covers a subset of the
762
+ * traffic. `heat:screen-coverage` reports that subset's size per app for exactly this
763
+ * reason.
764
+ */
751
765
  const event: JourneyEvent = {
752
766
  eventId: generateEventId(),
753
767
  // sessionId is (re)stamped by the analytics lane at flush time.
@@ -849,7 +863,32 @@ export class SessionManager {
849
863
  } catch { /* no-throw */ }
850
864
  }
851
865
 
852
- const event = this.pipeline.emit(type, opts);
866
+ /**
867
+ * Which frame was on screen when this happened.
868
+ *
869
+ * Resolved BEFORE `pipeline.emit`, and deliberately before the capture triggers
870
+ * below: those capture the frame that shows the RESULT of the interaction, and a tap
871
+ * belongs to the state it was made IN, not the one it produced. Reading after them
872
+ * would attribute every tap to its own consequence.
873
+ *
874
+ * USER_ACTION only. A SCREEN_VIEW's frame is the screen being left, which is
875
+ * misleading rather than useful, and no consumer asks for it.
876
+ *
877
+ * `currentKeyframe` returns null unless the frame is from this same screen and still
878
+ * fresh — see its contract. Null is the common, correct answer and stays absent on
879
+ * the event rather than becoming a default.
880
+ */
881
+ const framedOpts =
882
+ type === 'USER_ACTION'
883
+ ? {
884
+ ...opts,
885
+ frameId: this.captureManager?.currentKeyframe(
886
+ opts?.screen ?? this._lastKnownScreen ?? undefined,
887
+ )?.frameId,
888
+ }
889
+ : opts;
890
+
891
+ const event = this.pipeline.emit(type, framedOpts);
853
892
 
854
893
  // Trigger capture on screen view or interaction
855
894
  if (event) {
@@ -4,6 +4,7 @@
4
4
  * Provides the callback for the SessionManager to request screenshots
5
5
  * from the native layer (PixelCopy on Android, UIWindow on iOS).
6
6
  */
7
+ import { resolvePlatformOS, type PlatformOS } from '../../core/context/device';
7
8
 
8
9
  export const nativeCapture = async (sessionId: string, reason: string, _quality: any) => {
9
10
  try {
@@ -17,8 +18,8 @@ export const nativeCapture = async (sessionId: string, reason: string, _quality:
17
18
  const result = await replaySdk.captureScreenshot(reason);
18
19
  if (!result?.base64) return null;
19
20
 
20
- const { Platform } = require('react-native');
21
- const platform: 'android' | 'ios' = Platform.OS === 'ios' ? 'ios' : 'android';
21
+ // Platform.OS verbatim. The ternary this replaces labelled every non-iOS frame 'android'.
22
+ const platform: PlatformOS = resolvePlatformOS();
22
23
 
23
24
  return {
24
25
  base64: result.base64 as string,
@@ -15,13 +15,14 @@
15
15
  * transparently falls back to the legacy `captureScreenshot` path.
16
16
  */
17
17
 
18
- import { Platform } from 'react-native';
19
18
  import { getReplaySdkNative } from '../replay/bridge/nativeModule';
19
+ import { resolvePlatformOS, type PlatformOS } from '../../core/context/device';
20
20
  import type { OutboxFrameCapture, StagedFrameResult, OutboxFrameMeta } from './ReplayCaptureManager';
21
21
 
22
22
  /** Build the adapter. `ensureReady` resolves false when the native outbox is off. */
23
23
  export function makeOutboxFrameCapture(ensureReady: () => Promise<boolean>): OutboxFrameCapture {
24
- const platform: 'android' | 'ios' = Platform.OS === 'ios' ? 'ios' : 'android';
24
+ // Platform.OS verbatim see core/context/device.
25
+ const platform: PlatformOS = resolvePlatformOS();
25
26
 
26
27
  return {
27
28
  async capture(sessionId, screenName, trigger): Promise<StagedFrameResult | null> {
@@ -1,3 +1,4 @@
1
+ import type { PlatformOS } from '../../core/context/device';
1
2
  /**
2
3
  * ScaleBun SDK — Unified Session Domain Model
3
4
  *
@@ -21,7 +22,9 @@ export interface SessionMetadata {
21
22
  appName?: string;
22
23
  appVersion?: string;
23
24
  bundleId?: string;
24
- platform?: 'android' | 'ios' | 'web';
25
+ // Widened to the real Platform.OS — see core/context/device. The wire's platform field is an
26
+ // open enum, so a value this union did not anticipate travels rather than being coerced.
27
+ platform?: PlatformOS;
25
28
  osVersion?: string;
26
29
  deviceModel?: string;
27
30
  deviceName?: string;
@@ -78,6 +81,22 @@ export interface JourneyEvent {
78
81
  severity?: JourneyEventSeverity;
79
82
  /** Screen name at time of event */
80
83
  screen?: string;
84
+ /**
85
+ * frameId of the screenshot that was ON SCREEN when this event happened.
86
+ *
87
+ * The picture the user was looking at, not the one that follows. Mobile has no DOM to
88
+ * reconstruct, so a captured frame is the only record of what a screen looked like at a
89
+ * moment — which makes this the join key between a tap and the state it was made in.
90
+ *
91
+ * Set only when a keyframe from the SAME screen was captured recently enough to still be
92
+ * showing (see SessionManager.currentFrameForEvent). Absent means "not captured", which is a
93
+ * DIFFERENT answer from "nothing was open" and must never be folded into it:
94
+ * · the analytics lane never has one — it runs only when no recording is active, so no
95
+ * screenshots exist at all
96
+ * · a tap right after navigation has none — the capture is debounced and evicted on
97
+ * screen change
98
+ */
99
+ frameId?: string;
81
100
  payload?: Record<string, unknown>;
82
101
  /** Links related events (e.g. network request start → complete) */
83
102
  correlationId?: string;
@@ -114,7 +133,9 @@ export interface ReplayFrame {
114
133
  quality: ReplayQuality;
115
134
  captureReason: CaptureReason;
116
135
  /** Native-only capture — JS never captures frames */
117
- sourcePlatform: 'android' | 'ios';
136
+ // Widened: a frame reports the REAL Platform.OS. The old union forced every non-iOS capture to
137
+ // be relabelled 'android', which is how RN-macOS and RN-Windows sessions became Android ones.
138
+ sourcePlatform: PlatformOS;
118
139
  redactionState?: 'none' | 'text-masked' | 'full';
119
140
  /** Screen name at capture time — for frame-to-screen correlation */
120
141
  screen?: string;
@@ -363,7 +384,9 @@ export interface NativeFrameResult {
363
384
  /** Byte size of the compressed image */
364
385
  byteSize: number;
365
386
  /** Platform that performed the capture */
366
- platform: 'android' | 'ios';
387
+ // Widened from 'android'|'ios': a frame is labelled with the REAL Platform.OS now, and the
388
+ // old union forced every non-iOS capture to be relabelled 'android'.
389
+ platform: PlatformOS;
367
390
  /** Actual on-device encoding after the AVIF->WebP->JPEG fallback (P2-2). */
368
391
  format?: string;
369
392
  /** Optional native frame hash for change detection (P2-1). JS falls back to hashing base64. */
package/src/index.ts CHANGED
@@ -49,6 +49,9 @@ export {
49
49
  } from './features/engage/EngageAnchor';
50
50
 
51
51
  // ─── OTA Updates ───────────────────────────────────────────────────────────
52
+ // D3: manual capture — existed for a year, exported to nobody, delivered to a
53
+ // queue nothing drained. Now rides the proven crash pipeline.
54
+ export { captureException, captureMessage } from './features/crash/capture';
52
55
  export { otaOrchestrator, OtaOrchestrator } from './features/ota/OtaOrchestrator';
53
56
  export type { SyncResult, BootGuardConfig } from './features/ota/OtaOrchestrator';
54
57
  export { useOtaUpdate } from './features/ota/useOtaUpdate';
@@ -13,10 +13,14 @@ import { noThrow, safeAsync } from '../core/lifecycle/crashSafe';
13
13
  import { logger } from '../core/logger/internalLogger';
14
14
  import { createEnvelope } from '../pipeline/envelope/envelope';
15
15
  import { generateEventId } from '../features/replay/core/ids/sessionId';
16
+ import { buildBugReportPayload } from '../features/bugreport/buildPayload';
17
+ import type { BugSubmitResult } from '../features/bugreport/BugReportFeature';
18
+ import { BugReportFeature } from '../features/bugreport/BugReportFeature';
16
19
  import { PersistentQueue } from '../pipeline/queue/persistentQueue';
17
20
  import { MemoryBackend, createStorageBackend } from '../storage/StorageBackend';
18
21
  import { HttpClient } from '../transport/http/httpClient';
19
22
  import { getDeviceId } from '../core/id/deviceId';
23
+ import { mergeDeviceContext } from '../core/context/device';
20
24
  import { coreContainer } from '../core/di/container';
21
25
  import { flushQueue } from '../pipeline/queue/flushQueue';
22
26
  import {
@@ -278,7 +282,9 @@ class ScaleBunFacade {
278
282
  const appId = rawConfig?.appId;
279
283
  const enabled = rawConfig?.eventTracking !== false;
280
284
  const clientKey = this._clientKey ?? rawConfig?.clientKey;
281
- const apiBaseUrl = this._apiBaseUrl ?? rawConfig?.apiBaseUrl;
285
+ // apiBaseUrl is LOCKED use only the bootstrapped value, never fall back to raw
286
+ // integrator input, so the event-tracker/SKAdNetwork lane can't be repointed either.
287
+ const apiBaseUrl = this._apiBaseUrl;
282
288
  if (!enabled || !appId || !clientKey || !apiBaseUrl) {
283
289
  if (enabled && !appId) {
284
290
  logger.info('[ScaleBun] Envelope event tracking disabled — no appId in init config.');
@@ -307,7 +313,16 @@ class ScaleBunFacade {
307
313
  automaticEventTracking:
308
314
  rawConfig?.automaticEventTracking === true,
309
315
  flushIntervalMs: rawConfig?.flushIntervalMs,
310
- context: rawConfig?.context,
316
+ /**
317
+ * DETECTED, not configured.
318
+ *
319
+ * This was `rawConfig?.context` — whatever the app typed. Production carries
320
+ * `device_model: "android"` and `manufacturer: "android"` from a hand-written
321
+ * literal, while the replay lane was reading the real values off the native bridge
322
+ * the whole time. The SDK knows the device; the integrator is guessing at it.
323
+ * Everything outside the detected keys is still theirs.
324
+ */
325
+ context: mergeDeviceContext(rawConfig?.context),
311
326
  // Use the same foreground id as replay/journey capture. The
312
327
  // callback is resolved per event because the session subsystem
313
328
  // may start after this tracker is constructed.
@@ -597,6 +612,125 @@ class ScaleBunFacade {
597
612
  return null;
598
613
  }
599
614
 
615
+ // ─── Bug reports (F-BUG) ────────────────────────────────────────────
616
+
617
+ /**
618
+ * Thin feature object; the facade owns the HTTP, mirroring the web SDK split.
619
+ * Its only job here is raising the 'report' capture signal so replay flushes.
620
+ */
621
+ private readonly _bugReport = new BugReportFeature();
622
+
623
+ /**
624
+ * Submit a bug report against the CURRENT session.
625
+ *
626
+ * Mirrors the web SDK's `reportBugDetailed` ordering, which is load-bearing rather than
627
+ * incidental:
628
+ *
629
+ * 1. resolve the session — a report with no session cannot be joined to its evidence
630
+ * 2. signal 'report' — replay flushes its buffer so the report has visual context
631
+ * 3. snapshot the anchor — BEFORE the POST. A flush asks the recorder to deliver pending
632
+ * bytes; it cannot retroactively manufacture a frame, so an absent
633
+ * coordinate must stay absent rather than becoming a misleading
634
+ * "start of session" link in the dashboard
635
+ * 4. build + POST — bounds enforced by buildBugReportPayload against the server DTO
636
+ *
637
+ * Returns the report id, or `{ e }` naming which branch failed — 'init' (SDK not configured)
638
+ * reads differently from 'send' (network), and that difference is what tells an integrator
639
+ * whether to check their setup or their connection. It never throws: this is called from a UI
640
+ * the host app renders, usually from a user gesture, and an exception escaping into a React
641
+ * Native render tree is a redbox shown to the customer's user over a report they were sending us.
642
+ */
643
+ public async reportBugDetailed(input: {
644
+ message: string;
645
+ title?: string;
646
+ email?: string;
647
+ clientReportId?: string;
648
+ }): Promise<BugSubmitResult> {
649
+ if (!input || !input.message || input.message.trim().length === 0) {
650
+ logger.warn('[ScaleBun] reportBug: a message is required');
651
+ return { e: 'empty' };
652
+ }
653
+ if (!this._clientKey || !this._apiBaseUrl) {
654
+ logger.warn('[ScaleBun] reportBug: SDK is not configured (clientKey/apiBaseUrl)');
655
+ return { e: 'init' };
656
+ }
657
+
658
+ const sessionId = this.getActiveSessionId();
659
+ if (!sessionId) {
660
+ // No session means no row to attach to, and the ingestion route is session-scoped.
661
+ logger.warn('[ScaleBun] reportBug: no active session');
662
+ return { e: 'init' };
663
+ }
664
+
665
+ try {
666
+ // Flush replay into the session BEFORE reading the anchor below.
667
+ this._bugReport.signalReport();
668
+
669
+ const coord = this._replayCoord();
670
+ const payload = buildBugReportPayload(input, this._recentBreadcrumbs(), {
671
+ replayCoord: coord,
672
+ timestamp: Date.now(),
673
+ });
674
+
675
+ const res = await fetch(
676
+ `${this._apiBaseUrl}/ingestion/sessions/${sessionId}/bug-reports`,
677
+ {
678
+ method: 'POST',
679
+ headers: {
680
+ 'Content-Type': 'application/json',
681
+ 'x-scalebun-client-key': this._clientKey,
682
+ },
683
+ body: JSON.stringify(payload),
684
+ },
685
+ );
686
+ if (!res.ok) {
687
+ logger.warn('[ScaleBun] reportBug: server rejected the report', res.status);
688
+ return { e: 'send' };
689
+ }
690
+ const body = await res.json().catch(() => null);
691
+ return body?.id ?? { e: 'send' };
692
+ } catch (err: any) {
693
+ logger.warn('[ScaleBun] reportBug failed:', err?.message);
694
+ return { e: 'send' };
695
+ }
696
+ }
697
+
698
+ /**
699
+ * The replay coordinate at submit time, or null.
700
+ *
701
+ * Deliberately returns null rather than a zeroed coordinate when replay is disabled, paused, or
702
+ * has not produced a frame yet — see the anchor note on reportBug.
703
+ */
704
+ /**
705
+ * The replay coordinate at submit time.
706
+ *
707
+ * ALWAYS null today, deliberately and visibly.
708
+ *
709
+ * The web SDK reads this from its recorder, but the RN replay subsystem exposes no sequence or
710
+ * segment accessor on its public API (features/replay/public/api.ts) -- there is currently no
711
+ * supported way to ask it where playback is. Probing for it through an `any` cast would compile,
712
+ * miss, and leave every report anchor-less while looking wired: the same defect as the existing
713
+ * reportBug(), which emits an analytics event and looks like it files a report.
714
+ *
715
+ * Returning null keeps the payload HONEST -- buildBugReportPayload omits replaySeq entirely, so
716
+ * the dashboard shows no anchor rather than a false one pointing at the start of the session.
717
+ * To finish this: expose the current seq + segment id from the replay recorder, then read them
718
+ * here. The payload builder and its contract tests already handle a real coordinate, including
719
+ * a genuine seq of 0.
720
+ */
721
+ private _replayCoord(): { seq?: number | null; segmentId?: string | null } | null {
722
+ return null;
723
+ }
724
+ /** Recent console breadcrumbs for quick triage. The full set already travels with the session. */
725
+ private _recentBreadcrumbs(): readonly unknown[] {
726
+ try {
727
+ const f = (this as any)._loggerFeature ?? (this as any)._consoleFeature;
728
+ return f?.getRecent?.() ?? [];
729
+ } catch {
730
+ return [];
731
+ }
732
+ }
733
+
600
734
  // ─── Engage ─────────────────────────────────────────────────────────
601
735
 
602
736
  /**
@@ -1131,6 +1265,23 @@ class ScaleBunFacade {
1131
1265
 
1132
1266
  /** Report a bug (convenience wrapper over track) */
1133
1267
  public reportBug(description: string, metadata?: Record<string, unknown>) {
1268
+ // D11: this used to be ONLY `track('$bug_report')` — an analytics event
1269
+ // with zero consumers (no mapper, no handler, no BugReport row), so
1270
+ // /diagnose/bug-reports stayed empty forever for RN apps while the web
1271
+ // SDK used the real ingest. Route to the same endpoint the web SDK
1272
+ // uses; the analytics event is kept as a breadcrumb, not the delivery.
1273
+ try {
1274
+ const transport = SessionManager.getExistingInstance()?.getBackendTransport();
1275
+ transport?.submitBugReport({
1276
+ message: description,
1277
+ title:
1278
+ typeof metadata?.title === 'string' ? (metadata.title as string) : undefined,
1279
+ email:
1280
+ typeof metadata?.email === 'string' ? (metadata.email as string) : undefined,
1281
+ });
1282
+ } catch {
1283
+ /* delivery is best-effort; the track below still records intent */
1284
+ }
1134
1285
  return this.track('$bug_report', { description, ...metadata });
1135
1286
  }
1136
1287
 
@@ -23,8 +23,24 @@ export interface Spec extends TurboModule {
23
23
  /** DEV/TEST only: crash the process so the next launch can drain it. */
24
24
  crashNow(): Promise<boolean>;
25
25
 
26
+ /**
27
+ * Record which session is live so a crash can name it.
28
+ *
29
+ * Called once per session. Native writes it to crash-safe storage BEFORE any crash can happen —
30
+ * a fatal handler cannot call back into JS to ask.
31
+ */
32
+ setActiveSessionId(sessionId: string): Promise<boolean>;
33
+
26
34
  /** Return the previous run's crash record (or null), then clear it. */
27
35
  drainPendingCrash(): Promise<Object>;
36
+
37
+ /**
38
+ * Delete a delivered crash by its stable id.
39
+ *
40
+ * Called ONLY after the backend has accepted it. The read is non-destructive precisely so this
41
+ * can be the moment of deletion — a failed upload must leave the durable copy in place.
42
+ */
43
+ clearPendingCrash(crashId: string): Promise<boolean>;
28
44
  }
29
45
 
30
46
  /**