@everframe/react-native 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
40
40
  # 1.2.0, so the range already asks for 1.2 while package.json is still 1.1.0.
41
41
  # The 1.2.0 release moves package.json to match.
42
42
  native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
43
- native_minor_range = native_version_override.empty? ? '~> 1.2.0' : native_version_override
43
+ native_minor_range = native_version_override.empty? ? '~> 1.3.0' : native_version_override
44
44
 
45
45
  Pod::Spec.new do |s|
46
46
  s.name = 'EverframeRN'
package/README.md CHANGED
@@ -29,6 +29,25 @@ Then install pods (iOS) / sync Gradle (Android) as usual:
29
29
  cd ios && pod install
30
30
  ```
31
31
 
32
+ ### Source maps and symbols
33
+
34
+ Release builds can upload their Hermes source maps, dSYMs, R8 mappings and
35
+ native libraries, so crashes show your code. Wrap Metro with
36
+ `withEverframe` from `@everframe/metro`, add `@everframe/cli`, and set up the
37
+ native projects once. In Expo, apply `withEverframe` from `@everframe/expo`
38
+ with your App ID in `app.config.js`. In a bare app, run:
39
+
40
+ ```bash
41
+ npx @everframe/cli setup react-native --app-id "$EVERFRAME_APP_ID"
42
+ ```
43
+
44
+ Either way, Android gets `apply plugin: "dev.everframe"` and
45
+ `everframe { appId = "…" }` plus one classpath line, and Xcode gets a
46
+ one-line Run Script phase that runs `scripts/xcode-upload.sh` from the installed
47
+ `@everframe/cli`. The upload logic lives in the Gradle plugin and the CLI. Set `EVERFRAME_API_TOKEN` in release builds;
48
+ without it the build warns and continues. See the
49
+ [CLI guide](../cli/README.md#react-native-and-expo).
50
+
32
51
  ## React Native Web
33
52
 
34
53
  `@everframe/react-native` has a browser entry and an explicit
@@ -49,6 +68,8 @@ the web mask registry. Native iOS and Android use their native capture. A web
49
68
  bundler that honors the `browser` export condition can keep the regular package
50
69
  import; use the `/web` subpath if its resolver does not select that condition.
51
70
 
71
+ Replays also attach to errors; see [session replay](../../docs/session-replay.md).
72
+
52
73
  ## Usage
53
74
 
54
75
  Wrap your app with `<EverframeProvider>` at the highest practical level
@@ -283,15 +304,12 @@ Snapshots are detached, contain no captured content, reset on remount, and perfo
283
304
  no native query or upload. Paths are `handled` (including the boundary callback),
284
305
  `errorUtils` (including errors no boundary catches), and `rejection` (submission only). Each active attempt has one outcome:
285
306
  `accepted`, `duplicateSuppressed`, `allowanceSuppressed`, `bridgeUnavailable`,
286
- `nativeRefused`, `captureFailed`, `reentrantSuppressed`, `inactiveAborted`, or
287
- `legacyAttempted`. Counters saturate at 2,147,483,647. Existing limits are ten
307
+ `nativeRefused`, `captureFailed`, `reentrantSuppressed`, or `inactiveAborted`.
308
+ Counters saturate at 2,147,483,647. Existing limits are ten
288
309
  handled and ten automatic distinct keys; fatal capture bypasses them.
289
310
 
290
- `accepted` requires exactly true from a current native capture method. It does
291
- not mean upload success, server processing or outbox removal. An automatic call
292
- to an older native fallback is only `legacyAttempted`, regardless of its return.
293
- Old-binary unavailable/throwing methods are covered by unit controls, not an
294
- installed old-binary qualification. This snapshot excludes native queue and
311
+ `accepted` requires exactly true from the native capture method. It does
312
+ not mean upload success, server processing or outbox removal. This snapshot excludes native queue and
295
313
  delivery observations; read those with `getReportDeliveryStatus()`. Status is
296
314
  neutral `not-mounted` without a provider, `disabled` under the crash-reporting
297
315
  veto, and `unsupported` on the browser root entry.
@@ -341,8 +359,7 @@ bridges. iOS marks `jvm-uncaught` unsupported and implies no automatic iOS nativ
341
359
  crash collector.
342
360
 
343
361
  Without a mounted provider the result is `not-mounted/no-mount`; the browser
344
- export is `unsupported/platform`. Older native SDKs without the getter return
345
- `unsupported/native-method-missing`. A throwing native call returns
362
+ export is `unsupported/platform`. A missing or throwing native call returns
346
363
  `unavailable/native-call-failed`, and malformed native responses return
347
364
  `unavailable/invalid-native-snapshot`. These fallbacks have an unobserved queue
348
365
  and `unknown` platform policies. This API does not provide a per-report delivery
@@ -379,6 +396,34 @@ capturing network traffic in the first place:
379
396
  Do not enable this option expecting RN's own network calls to show up in
380
397
  reports — that capability doesn't exist yet.
381
398
 
399
+ ## Crash-only sessions
400
+
401
+ ```tsx
402
+ <EverframeProvider config={{ sdkKey: '…', reporter: { disabled: true } }}>
403
+ ```
404
+
405
+ `reporter.disabled: true` turns the bug reporter off for the session and
406
+ keeps crash and error reporting, for example in production builds. It is a
407
+ **client veto**: the dashboard can't turn the reporter back on for this
408
+ session.
409
+
410
+ In a crash-only session:
411
+
412
+ - Shake-to-report is off, and `open()` rejects with
413
+ `EverframeReporterDisabledError`.
414
+ - Session replay uses the app's crash-only sampling rate, which is 0 by
415
+ default. An error carries a replay only while the session records.
416
+ - Network bodies aren't captured.
417
+ - Crash and error capture, breadcrumbs, vitals, release health and the
418
+ companion work as usual.
419
+
420
+ A companion bond (a phone scanning the QR code, or a dashboard member
421
+ attaching) upgrades the session. Replay and network bodies then follow the
422
+ app's normal settings for the rest of the session, and recording starts at
423
+ the bond. The reporter stays off: reports from an upgraded session come
424
+ through the companion. See
425
+ [crash-only sessions](../../docs/session-replay.md#crash-only-sessions).
426
+
382
427
  ## Navigation breadcrumbs
383
428
 
384
429
  Mark screens with one line; the native SDK derives `from → to` from a global
@@ -472,15 +517,15 @@ inside a native view and never reaches JS.
472
517
  - `captureSourceQuery` — keep the query string on `source_change.src`
473
518
  (default `false`: signed CDN URLs carry tokens in the query).
474
519
 
475
- **Reconfiguring.** A Provider remount with the *same* config does not restart
476
- the SDK: both native sides compare the incoming config against the *installed*
520
+ **Reconfiguring.** A Provider remount with the _same_ config does not restart
521
+ the SDK: both native sides compare the incoming config against the _installed_
477
522
  one and skip the start entirely. A **changed** config does restart it (on iOS,
478
523
  except a change confined to `releaseHealth` or the loaded bundle; see
479
524
  [Release health](#release-health)) — and a
480
525
  start supersedes the running SDK, detaching every player integration it had
481
526
  announced. **Limitation:** players tracked before that restart stay untracked
482
527
  until their screens remount; nothing re-registers them automatically. Two React
483
- instances in one process are *not* coordinated either: if a second one starts
528
+ instances in one process are _not_ coordinated either: if a second one starts
484
529
  the SDK under the first, the first instance's players stay untracked until its
485
530
  own screens remount.
486
531
 
@@ -525,7 +570,7 @@ adapter has subscribed — and `onLoadStart` is what arms the adapter's startup
525
570
  clock, so the first source's startup latency is simply never seen. Deferring
526
571
  the load until after the adapter subscribed puts the first `onLoadStart` back
527
572
  where it can be observed. Without this, everything else still works; only the
528
- *first* `startup` measurement is missing (later source changes are fine).
573
+ _first_ `startup` measurement is missing (later source changes are fine).
529
574
 
530
575
  Options: `name`, `libraryVersion`, and `captureErrors` (default `false` — see
531
576
  Limitations). `onBandwidthUpdate` is read per platform: on iOS its `bitrate`
@@ -535,7 +580,7 @@ the `bandwidthEstimate` stat (with `quality_change` for the accompanying
535
580
  rendition size) and never as a bitrate.
536
581
 
537
582
  `onProgress` is read per platform too. The `bufferAheadMs` stat is always the
538
- media time buffered *ahead of the playhead*: on Android `bufferDuration` is
583
+ media time buffered _ahead of the playhead_: on Android `bufferDuration` is
539
584
  already that, on iOS it is the buffered range's absolute end position, so the
540
585
  adapter subtracts `currentTime` from it.
541
586
 
@@ -618,7 +663,7 @@ dropped, not forwarded.
618
663
  `captureErrors` is off by default.
619
664
  - **`rate_change` was not observed from react-native-video v7 on Android** in
620
665
  our Android emulator smoke — `onPlaybackRateChange` never arrived there.
621
- Note that the library *does* forward ExoPlayer's
666
+ Note that the library _does_ forward ExoPlayer's
622
667
  `onPlaybackParametersChanged`, so this is a "not seen in our smoke run"
623
668
  result, not a proven library gap; a host that changes playback speed
624
669
  explicitly may well see it. Nothing on the adapter side gates it either way.
@@ -693,7 +738,7 @@ subscription?.remove();
693
738
  ```
694
739
 
695
740
  > If you have seen the older `new TVEventHandler(); handler.enable(cmp, cb);
696
- > handler.disable()` shape elsewhere — that class-based API was removed in
741
+ handler.disable()` shape elsewhere — that class-based API was removed in
697
742
  > RN-tvos 0.85.x. Always read the version installed in your `node_modules`.
698
743
 
699
744
  #### Apple TV: long-press Play/Pause
@@ -777,28 +822,12 @@ recipes above.
777
822
  ## Android reporter initialization
778
823
 
779
824
  The Android reporter can be opened after the host activity has resumed, including
780
- apps that disable AndroidX Startup. Use matching updated React Native bridge and
781
- `dev.everframe:reporter-ui` artifacts: the bridge supplies the current resumed
825
+ apps that disable AndroidX Startup. The bridge supplies the current resumed
782
826
  activity, and installation and presentation run on the main thread. Repeated
783
- configuration with matching updated artifacts does not register another lifecycle observer. Capture configuration
784
- remains synchronous without waiting for reporter UI initialization.
785
-
786
- The existing `ReporterResolverInstaller.create(Context)` entry point is retained
787
- for compiled callers. A new bridge with an older reporter uses its legacy entry
788
- point when the activity-aware method is absent; that combination retains the old
789
- late-initialization limitation. A failure from an available method is reported,
790
- not retried through the legacy path. Background or unavailable activities return
791
- a cancelled result with reason `no_active_activity`.
792
-
793
- Legacy off-main `create(Context)` schedules installation and returns before it is
794
- ready; use the awaited activity-aware bridge to open immediately after configure.
795
- With a new bridge and an older reporter, each configure, companion start, and
796
- open invokes legacy installation again. This can add duplicate lifecycle
797
- callbacks and overwrite that older reporter's resolver and activity supplier;
798
- the previous bridge limited installation to once per module. Conversely, an old
799
- compiled bridge with the new reporter can announce companion PIN capability
800
- before asynchronous reporter installation finishes, temporarily advertising no
801
- PIN support. Use the matching updated pair for the qualified behavior.
827
+ configuration does not register another lifecycle observer. Capture configuration
828
+ remains synchronous without waiting for reporter UI initialization. A failure
829
+ while opening is reported to JavaScript. Background or unavailable activities
830
+ return a cancelled result with reason `no_active_activity`.
802
831
 
803
832
  Activity destruction while a reporter is open resolves the pending call with
804
833
  `{ status: "cancelled", reason: "activity_destroyed" }`, releases that report's
@@ -153,7 +153,7 @@ val rnSdkPkg = groovy.json.JsonSlurper()
153
153
  val rnSdkVersion = rnSdkPkg["version"] as String
154
154
  // Native 1.2.0 is the release that renamed the SDK key to `sdkKey`; the
155
155
  // bridge moves with it, ahead of package.json, which the 1.2.0 release bumps.
156
- val nativeMinorRange: String = nativeVersionOverride ?: "[1.2.0,1.3.0)"
156
+ val nativeMinorRange: String = nativeVersionOverride ?: "[1.3.0,1.4.0)"
157
157
  if (nativeVersionOverride != null) {
158
158
  logger.lifecycle(
159
159
  "[everframe-rn] native AAR pinned to $nativeVersionOverride via everframeNativeVersion " +
@@ -22,7 +22,7 @@
22
22
  // signalCompanionReportRequestReady(id) → void (Plan 06.2-13)
23
23
  // trackPlayer/detachPlayer/recordPlayerEvent/updatePlayerStats/
24
24
  // trackVitals → void (spec 2026-09-06)
25
- // configure(opts.vitals*) → void (spec 2026-09-06)
25
+ // configureSync(opts.vitals*) → boolean (spec 2026-09-06)
26
26
  // addListener(name)/removeListeners(n) → void (RN event-emitter no-ops)
27
27
  //
28
28
  // The native TXReporterPresenter owns the entire report UX (capture, annotate,
@@ -30,8 +30,7 @@
30
30
  // and the sensitive-rect bridge. Native mobile shake stays below this bridge.
31
31
  //
32
32
  // THREADING:
33
- // • configureSync runs on the JS thread (blocking synchronous); legacy
34
- // void configure retains React Native's queued native dispatch.
33
+ // • configureSync runs on the JS thread (blocking synchronous).
35
34
  // • openReporter dispatches presenter.openReporter onto the activity from
36
35
  // MainScope (presenter manages its own UI thread hops internally).
37
36
  // • registerSensitiveRect runs on the UI thread (UIManager view resolution).
@@ -148,6 +147,8 @@ class EverframeModule(
148
147
  internal const val REJECT_CAPTURE_DISABLED: String = "capture_disabled"
149
148
  internal const val REJECT_NO_ACTIVITY: String = "no_activity"
150
149
  internal const val REJECT_OPEN_REPORTER_FAILED: String = "open_reporter_failed"
150
+ /** Crash-only sessions (spec 2026-10-12). The JS facade maps this exact code to `EverframeReporterDisabledError`. */
151
+ internal const val REJECT_REPORTER_DISABLED: String = "EVERFRAME_REPORTER_DISABLED"
151
152
 
152
153
  // Plan 06.2-11 — JS-side event names. Keep in lock-step with
153
154
  // `packages/sdk-react-native/src/companion.ts` and the iOS
@@ -175,13 +176,10 @@ class EverframeModule(
175
176
  // below for the full contract.
176
177
  internal const val COMPANION_ATTACH_CHALLENGE_EVENT: String =
177
178
  "everframe.companion.attachChallenge"
178
- // Fired when a `report.request` arrives from the paired phone. This
179
- // used to give JS a window to walk the React fiber tree and call
180
- // `attachReactTree`; both the tree payload and that spec method are
181
- // gone (spec 2026-08-29). The event and its
182
- // `signalCompanionReportRequestReady` acknowledgement survive as the
183
- // companion's JS-ready handshake — the bounded wait is part of the
184
- // pairing flow, not of tap-to-identify.
179
+ // Fired when a `report.request` arrives from the paired phone. The
180
+ // event and its `signalCompanionReportRequestReady` acknowledgement
181
+ // are the companion's JS-ready handshake — the bounded wait is part
182
+ // of the pairing flow.
185
183
  internal const val COMPANION_REPORT_REQUESTED_EVENT: String = "everframe.companion.reportRequested"
186
184
 
187
185
  // Spec 2026-09-17 setExtra-resolver — NOT a companion event (no
@@ -209,11 +207,6 @@ class EverframeModule(
209
207
 
210
208
  // ---------------- configure ----------------
211
209
 
212
- @ReactMethod
213
- override fun configure(opts: ReadableMap) {
214
- configureInternal(opts)
215
- }
216
-
217
210
  @ReactMethod(isBlockingSynchronousMethod = true)
218
211
  override fun configureSync(opts: ReadableMap): Boolean = configureInternal(opts)
219
212
 
@@ -271,6 +264,10 @@ class EverframeModule(
271
264
  // Polarity flips exactly once, here: JS sends the veto
272
265
  // (`crashReportingDisabled`), native stores the permission.
273
266
  val crashReportingDisabled = opts.takeIfHasBoolean("crashReportingDisabled") ?: false
267
+ // Polarity flips exactly once, here: JS sends the crash-only switch
268
+ // (`reporterDisabled`, flattened from `reporter: { disabled }`),
269
+ // native stores the permission.
270
+ val reporterDisabled = opts.takeIfHasBoolean("reporterDisabled") ?: false
274
271
  // Attach-PIN UI mode (spec 2026-08-19) — a flat optional STRING
275
272
  // field (codegen cannot express a string-literal union), parsed
276
273
  // here and stashed for `startCompanion()` to read (the flag is a
@@ -314,6 +311,7 @@ class EverframeModule(
314
311
  // `startCompanion()` time, mirroring `attachPinUi`.
315
312
  companionBadgeEnabled = opts.takeIfHasBoolean("companionBadgeEnabled") ?: true,
316
313
  shakeToReportEnabled = opts.takeIfHasBoolean("shakeToReportEnabled") ?: true,
314
+ reporterEnabled = !reporterDisabled,
317
315
  companionBadgePosition = opts.takeIfHasString("companionBadgePosition"),
318
316
  // Inline reporter theme (reporter branding spec 2026-08-25,
319
317
  // RN slice) — 8 flat optional STRING fields on ConfigOpts
@@ -423,6 +421,9 @@ class EverframeModule(
423
421
  * { status: "submitted", reportId: "<uuid>" }
424
422
  * { status: "queued", reportId: "<uuid>" }
425
423
  * { status: "cancelled" }
424
+ *
425
+ * Rejects with [REJECT_REPORTER_DISABLED] in a crash-only session
426
+ * (`reporterDisabled`), including after a companion bond upgraded it.
426
427
  */
427
428
  @ReactMethod
428
429
  override fun openReporter(promise: Promise) {
@@ -430,6 +431,12 @@ class EverframeModule(
430
431
  promise.reject(REJECT_CAPTURE_DISABLED, "Everframe not started (captureGate=false). Call configure() first.")
431
432
  return
432
433
  }
434
+ // Crash-only session: rejected here because ReporterBridge reduces every core failure to one
435
+ // generic message, which would hide `ReporterDisabledException` from JS.
436
+ if (Everframe.currentConfig?.reporterEnabled == false) {
437
+ promise.reject(REJECT_REPORTER_DISABLED, "The reporter is turned off for this session (reporter.disabled).")
438
+ return
439
+ }
433
440
  MainScope().launch {
434
441
  try {
435
442
  val result: ReportResult = reporterBridge.open(reactContext)
@@ -500,7 +507,7 @@ class EverframeModule(
500
507
  * `Everframe.addBreadcrumb` (Task 9) — NO validation/coercion here. That
501
508
  * singleton already owns kind/level/data coercion AND gating (no-op
502
509
  * pre-start / while killed via its own `txGuardVoid` + `captureGate`
503
- * check), exactly like `setExtra`/`attachReactTree` above never re-check
510
+ * check), exactly like `setExtra` above never re-checks
504
511
  * `captureGate` themselves. `data` crosses as a `ReadableMap?` — the
505
512
  * codegen-typed freeform-object boundary — converted with RN's own
506
513
  * `toHashMap()` (no re-shaping) before handing off.
@@ -637,7 +644,7 @@ class EverframeModule(
637
644
  *
638
645
  * The primary fix is the JS projection in `src/user-projection.ts`, applied
639
646
  * before the value ever crosses the bridge. This is the second layer, for a
640
- * hand-wired native caller (or a host on an older JS bundle) that reaches
647
+ * hand-wired native caller that reaches
641
648
  * this method directly: bad fields degrade to `null` — a partial user, or
642
649
  * anonymous — and `Everframe.setUser` is ALWAYS reached, so the call always
643
650
  * replaces what was installed before.
@@ -687,11 +694,11 @@ class EverframeModule(
687
694
  @ReactMethod(isBlockingSynchronousMethod = true)
688
695
  override fun getReportDeliveryStatusJson(): String = Everframe.getReportDeliveryStatus().toJson()
689
696
 
690
- /** Automatic ErrorUtils capture; true acknowledges completed sidecar storage. */
697
+ /** Automatic ErrorUtils and unhandled-rejection capture; true acknowledges completed sidecar storage. A non-fatal error may carry a replay. */
691
698
  @ReactMethod(isBlockingSynchronousMethod = true)
692
699
  override fun reportCrash(crashJson: String): Boolean = captureCrashJson(crashJson, handled = false)
693
700
 
694
- /** Explicit capture always enforces handled=true, fatal=false, captureException. */
701
+ /** Explicit capture always enforces handled=true, fatal=false, captureException, and may carry a replay. */
695
702
  @ReactMethod(isBlockingSynchronousMethod = true)
696
703
  override fun captureHandledException(crashJson: String): Boolean = captureCrashJson(crashJson, handled = true)
697
704
 
@@ -718,12 +725,17 @@ class EverframeModule(
718
725
  val details = if (obj.has("details")) obj.get("details") else null
719
726
  val causeChain = if (obj.has("causeChain")) obj.get("causeChain") else null
720
727
  val fatal = !handled && obj.optBoolean("fatal", false)
721
- val accepted = if (handled) {
722
- dev.everframe.crash.CrashReporter.captureHandledFactsWithCauses(
728
+ // Only errors the app survives ask for a replay. Eligibility and export remain in the core;
729
+ // fatal errors keep the entry point that never carries a replay.
730
+ val accepted = when {
731
+ handled -> dev.everframe.crash.CrashReporter.captureHandledFactsWithReplay(
723
732
  exceptionType, message, frames, occurredAt, jsBundle, details, causeChain, "everframe-android",
724
733
  )
725
- } else {
726
- dev.everframe.crash.CrashReporter.captureFactsAcceptedWithCauses(
734
+ fatal -> dev.everframe.crash.CrashReporter.captureFactsAcceptedWithCauses(
735
+ exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
736
+ fatal, occurredAt, jsBundle, details, causeChain,
737
+ )
738
+ else -> dev.everframe.crash.CrashReporter.captureFactsAcceptedWithReplay(
727
739
  exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
728
740
  fatal, occurredAt, jsBundle, details, causeChain,
729
741
  )
@@ -1097,14 +1109,6 @@ class EverframeModule(
1097
1109
  val capture: ScreenshotCapture.CaptureResult,
1098
1110
  val reportCapture: dev.everframe.capture.video.FrozenReportCapture,
1099
1111
  val hostExtra: String?,
1100
- /**
1101
- * Drained-and-discarded host React tree (spec 2026-08-29). Nothing
1102
- * reads it any more, and RN can no longer attach one — the
1103
- * TurboModule spec method is gone. Kept only because the native core
1104
- * `Everframe.attachReactTree` remains callable by pure-native hosts,
1105
- * so the consume-once drain must still clear the slot.
1106
- */
1107
- val hostReactTreeJson: String?,
1108
1112
  )
1109
1113
 
1110
1114
  private val companionCaptureStash = ConcurrentHashMap<String, CompanionCapture>()
@@ -1161,7 +1165,7 @@ class EverframeModule(
1161
1165
  // same reason the screenshot capture above is —
1162
1166
  // this whole provider runs on the OkHttp WS
1163
1167
  // thread, never the UI thread.
1164
- val (hostExtraRaw, hostReactTreeJson) =
1168
+ val hostExtraRaw =
1165
1169
  runBlocking { Everframe.consumePendingAttachments() }
1166
1170
  val hostExtra = hostExtraRaw?.takeIf { it.isNotEmpty() }
1167
1171
 
@@ -1173,7 +1177,6 @@ class EverframeModule(
1173
1177
  capture = capture,
1174
1178
  reportCapture = reportCapture,
1175
1179
  hostExtra = hostExtra,
1176
- hostReactTreeJson = hostReactTreeJson?.takeIf { it.isNotEmpty() },
1177
1180
  )
1178
1181
  }
1179
1182
  if (!published) {
@@ -1485,9 +1488,7 @@ class EverframeModule(
1485
1488
  /**
1486
1489
  * Called by the JS companion module from the `everframe.companion
1487
1490
  * .reportRequested` event handler once it is ready for capture to
1488
- * proceed. (It used to walk the React fiber tree and call
1489
- * `attachReactTree` first; both are gone — spec 2026-08-29.) Releases
1490
- * the `awaitJsReactTreeAttach` waiter for this correlation id so the
1491
+ * proceed. Releases the `awaitJsReactTreeAttach` waiter for this correlation id so the
1491
1492
  * capture provider can proceed. Idempotent — late signals after the
1492
1493
  * timeout fired are harmless.
1493
1494
  */
@@ -9,7 +9,6 @@ import android.os.Looper
9
9
  import android.util.Log
10
10
  import com.facebook.react.bridge.ReactApplicationContext
11
11
  import com.facebook.react.common.LifecycleState
12
- import dev.everframe.Everframe
13
12
  import dev.everframe.config.ReportResult
14
13
  import java.lang.reflect.InvocationTargetException
15
14
  import java.lang.reflect.Method
@@ -19,7 +18,7 @@ import kotlinx.coroutines.CancellationException
19
18
  import kotlinx.coroutines.Dispatchers
20
19
  import kotlinx.coroutines.withContext
21
20
 
22
- /** Reflection preserves compatibility with separately built native reporter artifacts. */
21
+ /** Resolves the reporter installer reflectively, so tests can substitute it. */
23
22
  internal class ReporterBridge(
24
23
  private val resolveInstaller: () -> Class<*> = { Class.forName("dev.everframe.ui.ReporterResolverInstaller") },
25
24
  ) {
@@ -46,17 +45,9 @@ internal class ReporterBridge(
46
45
  try {
47
46
  val type = installerType()
48
47
  val instance = type.getDeclaredConstructor().newInstance()
49
- val method = try {
50
- type.getMethod("openForActivity", Context::class.java, Activity::class.java, Continuation::class.java)
51
- } catch (_: NoSuchMethodException) { null }
52
- if (method == null) {
53
- // Absence is the only legacy fallback. A present method's failure is final.
54
- invoke(type.getMethod("create", Context::class.java), instance, context.applicationContext)
55
- Everframe.report.open()
56
- } else {
57
- suspendCoroutineUninterceptedOrReturn<ReportResult> { continuation ->
58
- invoke(method, instance, context.applicationContext, activity, continuation)
59
- }
48
+ val method = type.getMethod("openForActivity", Context::class.java, Activity::class.java, Continuation::class.java)
49
+ suspendCoroutineUninterceptedOrReturn<ReportResult> { continuation ->
50
+ invoke(method, instance, context.applicationContext, activity, continuation)
60
51
  }
61
52
  } catch (failure: Throwable) {
62
53
  when (failure) {
@@ -89,6 +89,13 @@ type ConfigOpts = {
89
89
  companionBadgeEnabled?: boolean;
90
90
  /** Local veto for native mobile shake-to-report. Absent defaults to enabled. */
91
91
  shakeToReportEnabled?: boolean;
92
+ /**
93
+ * Crash-only session switch. `true` turns off every on-device way to open the reporter, keeps
94
+ * network bodies off and samples replay with the server's crash-only rate until a companion
95
+ * bond upgrades the session. Flattened by `extractBridgeConfig` from `reporter: { disabled }`.
96
+ * Absent means a normal report session.
97
+ */
98
+ reporterDisabled?: boolean;
92
99
  /**
93
100
  * Companion on-screen name-badge corner (naming spec 2026-08-24,
94
101
  * controller ruling / Task 6b). Flat optional STRING, not a literal
@@ -170,11 +177,10 @@ type EverframeUserSpec = {
170
177
  displayName?: string;
171
178
  };
172
179
  interface Spec extends TurboModule {
173
- configure(opts: ConfigOpts): void;
174
180
  /**
175
- * Synchronous startup entry used by the current JS runtime. The legacy
176
- * void configure method remains for already-compiled native callers, but
177
- * React Native dispatches void TurboModule methods asynchronously.
181
+ * Synchronous startup entry. It is blocking rather than void because React
182
+ * Native dispatches void TurboModule methods asynchronously, and the first
183
+ * capture after mount must not overtake configuration.
178
184
  */
179
185
  configureSync(opts: ConfigOpts): boolean;
180
186
  /**
@@ -261,6 +267,12 @@ interface Spec extends TurboModule {
261
267
  * fatal, occurredAt }. Native builds/redacts/persists the envelope with
262
268
  * state it already owns (crumb ring, device metadata, outbox).
263
269
  * Returns true when the report was persisted.
270
+ *
271
+ * A non-fatal ErrorUtils error or unhandled promise rejection may carry a
272
+ * session replay of the seconds before it. A fatal error never carries one.
273
+ * Native decides replay eligibility; JavaScript sends no extra fields.
274
+ * Send `fatal` as a JSON boolean: only an absent flag or boolean false can
275
+ * request a replay on iOS.
264
276
  */
265
277
  reportCrash(crashJson: string): boolean;
266
278
  /** Register a remote player. A token already live is ignored. */
@@ -288,7 +300,10 @@ interface Spec extends TurboModule {
288
300
  * `token` resolves to a live player, else session-scoped.
289
301
  */
290
302
  trackVitals(name: string, dataJson?: string, token?: string): void;
291
- /** Explicit handled capture; native enforces handled=true, fatal=false. */
303
+ /**
304
+ * Explicit handled capture; native enforces handled=true, fatal=false. Like a
305
+ * non-fatal `reportCrash`, it may carry a session replay.
306
+ */
292
307
  captureHandledException(crashJson: string): boolean;
293
308
  startCompanion(): void;
294
309
  stopCompanion(): void;
@@ -0,0 +1 @@
1
+ import{a as s,c as i,e as u}from"./chunk-UN3Q4UGZ.js";import{a as t}from"./chunk-XUSV3KOD.js";var n=class extends Error{static{t(this,"EverframeNotMountedError")}name="EverframeNotMountedError";constructor(o){super(o??"EverframeProvider is not mounted; wrap your app root.")}},c="EVERFRAME_REPORTER_DISABLED",a=class extends Error{static{t(this,"EverframeReporterDisabledError")}name="EverframeReporterDisabledError";constructor(o){super(o??"The Everframe reporter is disabled in this session (reporter.disabled).")}};var r=null,p={};function f(){return p}t(f,"__getContextEpoch");function g(){try{return r?.getPromiseRejectionStatus()??u("not-mounted","no-mount")}catch{return u("install-failed","hook-install")}}t(g,"getPromiseRejectionStatus");function R(e){r=e,p={}}t(R,"__setCurrentContext");function S(){return r}t(S,"__getCurrentContext");function y(){let e=r;return e?e.open():Promise.reject(new n("EverframeProvider not mounted; wrap your root with <EverframeProvider>."))}t(y,"open");function C(e){r?.setExtra(e)}t(C,"setExtra");function b(e){r?.addBreadcrumb(e)}t(b,"addBreadcrumb");function P(e,o){o!==void 0?r?.recordScreen(e,o):r?.recordScreen(e)}t(P,"recordScreen");function _(e){r?.setUser(e)}t(_,"setUser");function j(e,o){r?.captureException(e,o)}t(j,"captureException");function w(){try{return r?.getErrorCaptureStatus()??i("not-mounted","no-mount")}catch{return i("not-mounted","no-mount")}}t(w,"getErrorCaptureStatus");function D(){let e=r;try{let o=e?.getReportDeliveryStatus();return e===r&&o?o:s("not-mounted","no-mount")}catch{return s("unavailable","native-call-failed")}}t(D,"getReportDeliveryStatus");export{n as a,c as b,a as c,f as d,g as e,R as f,S as g,y as h,C as i,b as j,P as k,_ as l,j as m,w as n,D as o};
@@ -0,0 +1 @@
1
+ import{a as o}from"./chunk-XUSV3KOD.js";var h=["native-handled","bridge-handled","bridge-automatic","jvm-uncaught"],K=["persisted","disabled","admission-suppressed","reentrant","invalid-input","ownership-lost","storage-unavailable","failed"],S=["enqueue-committed","enqueue-failed","capacity-evicted","removed-after-acceptance","removed-after-terminal","removal-failed","read-failed"],T=["capacity","key-unavailable","corrupt","io","revoked","invalid-entry","busy","unknown","unsupported-format"],R=["live-submit","outbox-drain"],A=["server-accepted","retryable-http","terminal-http","network-failure","authorization-cancelled","cancelled","failed"],V=JSON.parse,H=Object.keys,y=Object.hasOwn,Q=Array.isArray,U=Number.isInteger,I=Reflect.apply,z=RegExp.prototype.test,_=/[^\x00-\x7f]/,O=2147483647;function d(e,t){let r={};for(let n=0;n<e.length;n++){let a=e[n];r[a]=t(a)}return r}o(d,"mapKeys");function J(e,t){return{schemaVersion:1,status:e,reason:t,scope:"native-runtime-observations",coverage:"best-effort",revision:0,capture:{enabled:!1,paths:d(h,()=>({supported:!1,settledAttempts:0,outcomes:d(K,()=>0)}))},queue:{scope:"sdk-report-outbox",observation:"not-observed",quality:"unknown",capacityPolicy:"unknown",terminalHttpPolicy:"unknown",operations:d(S,()=>0)},transport:d(R,()=>({settledAttempts:0,outcomes:d(A,()=>0)}))}}o(J,"emptyReportDeliveryStatus");function c(e,t,r=[]){if(!e||typeof e!="object"||Q(e))throw new Error;let n=H(e);for(let a=0;a<t.length;a++)if(!y(e,t[a]))throw new Error;for(let a=0;a<n.length;a++){let u=n[a];if(!g(t,u)&&!g(r,u))throw new Error}return e}o(c,"record");function g(e,t){for(let r=0;r<e.length;r++)if(e[r]===t)return!0;return!1}o(g,"contains");function l(e,t){if(typeof e!="string"||!g(t,e))throw new Error;return e}o(l,"enumeration");function v(e){if(typeof e!="number"||!U(e)||e<0||e>O)throw new Error;return e}o(v,"counter");function P(e){if(typeof e!="boolean")throw new Error;return e}o(P,"boolean");function q(e,t){let r=c(e,t);return d(t,n=>v(r[n]))}o(q,"counters");function j(e,t){let r=q(e.outcomes,t),n=v(e.settledAttempts),a=0;for(let b=0;b<t.length;b++)a+=r[t[b]];if((a>O?O:a)!==n)throw new Error;let u=y(e,"lastOutcome")?l(e.lastOutcome,t):void 0;if(n>0!=(u!==void 0)||u!==void 0&&r[u]===0)throw new Error;return{settledAttempts:n,outcomes:r,...u===void 0?{}:{lastOutcome:u}}}o(j,"settled");function C(e,t,r){for(let n=0;n<t.length;n++)if(r(e[t[n]])!==0)return!0;return!1}o(C,"nonzero");function M(e){try{if(typeof e!="string"||e.length>16384||I(z,_,[e]))return null;let t=c(V(e),["schemaVersion","status","reason","scope","coverage","revision","capture","queue","transport"]);if(t.schemaVersion!==1||t.scope!=="native-runtime-observations"||t.coverage!=="best-effort")return null;let r=l(t.status,["active","not-started","disabled","unavailable"]),n=l(t.reason,["none","no-start","capture-disabled","snapshot-busy"]);if({active:"none","not-started":"no-start",disabled:"capture-disabled",unavailable:"snapshot-busy"}[r]!==n)return null;let a=c(t.capture,["enabled","paths"]),u=c(a.paths,h),b=d(h,p=>{let f=c(u[p],["supported","settledAttempts","outcomes"],["lastOutcome"]),m={supported:P(f.supported),...j(f,K)};if(!m.supported&&m.settledAttempts!==0)throw new Error;return m}),i=c(t.queue,["scope","observation","quality","capacityPolicy","terminalHttpPolicy","operations"],["pendingCount","lastFailure"]);if(i.scope!=="sdk-report-outbox")return null;let s={scope:"sdk-report-outbox",observation:l(i.observation,["not-observed","observed","failed"]),quality:l(i.quality,["complete","partial","unknown"]),capacityPolicy:l(i.capacityPolicy,["reject-new","evict-oldest"]),terminalHttpPolicy:l(i.terminalHttpPolicy,["retain","attempt-remove"]),operations:q(i.operations,S),...y(i,"pendingCount")?{pendingCount:v(i.pendingCount)}:{},...y(i,"lastFailure")?{lastFailure:l(i.lastFailure,T)}:{}};if(s.capacityPolicy==="reject-new"!=(s.terminalHttpPolicy==="retain")||(s.observation!=="observed"||s.quality==="unknown")&&s.pendingCount!==void 0||s.observation!=="observed"&&s.quality!=="unknown"||s.observation==="failed"!=(s.lastFailure!==void 0))return null;let D=c(t.transport,R),E=d(R,p=>{let f=c(D[p],["settledAttempts","outcomes"],["lastOutcome","lastHttpStatus"]),m=j(f,A);if(y(f,"lastHttpStatus")){let x=v(f.lastHttpStatus);if(x<100||x>599||!g(["server-accepted","retryable-http","terminal-http"],m.lastOutcome??""))throw new Error;m.lastHttpStatus=x}return m}),w={schemaVersion:1,status:r,reason:n,scope:"native-runtime-observations",coverage:"best-effort",revision:v(t.revision),capture:{enabled:P(a.enabled),paths:b},queue:s,transport:E};return r!=="active"&&(w.revision!==0||w.capture.enabled||s.observation!=="not-observed"||C(b,h,p=>p.settledAttempts)||C(E,R,p=>p.settledAttempts)||C(s.operations,S,p=>p))?null:w}catch{return null}}o(M,"parseReportDeliveryStatus");function k(){return{attempted:0,accepted:0,duplicateSuppressed:0,allowanceSuppressed:0,bridgeUnavailable:0,nativeRefused:0,captureFailed:0,reentrantSuppressed:0,inactiveAborted:0}}o(k,"emptyCounters");function N(e,t){return{status:e,reason:t,scope:"mounted-js-admission",limits:{handled:10,automatic:10},counters:{handled:k(),errorUtils:k(),rejection:k()}}}o(N,"emptyErrorCaptureStatus");function F(e,t){e[t]=Math.min(2147483647,e[t]+1)}o(F,"incrementErrorCaptureCounter");function B(){let e=N("active","none").counters;return{begin(t){F(e[t],"attempted");let r=!1;return n=>{r||(r=!0,F(e[t],n))}},snapshot(){return{handled:{...e.handled},errorUtils:{...e.errorUtils},rejection:{...e.rejection}}}}}o(B,"createErrorCaptureLedger");function Y(e,t){return{status:e,reason:t,previousCallbacksPresent:!1,counters:{pending:0,accepted:0,cancelled:0,capacityDropped:0,sizeDropped:0,expired:0,duplicateSuppressed:0,allowanceSuppressed:0,nativeRefused:0,captureFailed:0,discarded:0}}}o(Y,"emptyPromiseRejectionStatus");export{J as a,M as b,N as c,B as d,Y as e};