@everframe/react-native 0.9.0 → 1.1.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.
@@ -36,7 +36,7 @@ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
36
36
  # no workspace path exists. Unset — every consumer install — the derivation
37
37
  # below is the only source of truth, unchanged.
38
38
  native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
39
- native_minor_range = native_version_override.empty? ? '~> 0.9.0' : native_version_override
39
+ native_minor_range = native_version_override.empty? ? '~> 1.1.0' : native_version_override
40
40
 
41
41
  Pod::Spec.new do |s|
42
42
  s.name = 'EverframeRN'
package/README.md CHANGED
@@ -26,6 +26,26 @@ Then install pods (iOS) / sync Gradle (Android) as usual:
26
26
  cd ios && pod install
27
27
  ```
28
28
 
29
+ ## React Native Web
30
+
31
+ `@everframe/react-native` has a browser entry and an explicit
32
+ `@everframe/react-native/web` entry. Both re-export the existing
33
+ `@everframe/react` package, which is installed as a dependency. Your app
34
+ imports from `@everframe/react-native` on every platform:
35
+
36
+ ```sh
37
+ pnpm add @everframe/react-native
38
+ ```
39
+
40
+ React Native Web hosts also need React DOM, as usual. The browser entry uses
41
+ the React web reporter, DOM screenshot, and rrweb replay. It does not load the
42
+ native TurboModule. `EverframeSensitive` is an alias of
43
+ the React SDK's `Sensitive` component on web, so shared app code can keep the
44
+ same import. `useEverframeSensitiveRef` also registers a browser element with
45
+ the web mask registry. Native iOS and Android use their native capture. A web
46
+ bundler that honors the `browser` export condition can keep the regular package
47
+ import; use the `/web` subpath if its resolver does not select that condition.
48
+
29
49
  ## Usage
30
50
 
31
51
  Wrap your app with `<EverframeProvider>` at the highest practical level
@@ -76,6 +96,210 @@ provider context and attach any non-sensitive context needed for diagnosis.
76
96
  The source API requires matching rebuilt native components; published `0.7.0`
77
97
  artifacts are not evidence of support.
78
98
 
99
+ ## Promise rejection observation
100
+
101
+ Automatic Hermes rejection observation is opt-in:
102
+
103
+ ```tsx
104
+ import { EverframeProvider, getPromiseRejectionStatus } from '@everframe/react-native';
105
+
106
+ <EverframeProvider config={{
107
+ apiKey: '…',
108
+ crashReporting: { promiseRejections: { enabled: true } },
109
+ }}>
110
+ <App />
111
+ </EverframeProvider>
112
+
113
+ // Read after mount. This reports observer coverage, not delivery confirmation.
114
+ const status = getPromiseRejectionStatus();
115
+ ```
116
+
117
+ The verified runtime is Android/iOS `react-native-tvos@0.85.3-0` with Hermes
118
+ release `250829098.0.10`, Release/Static Hermes and bytecode 98. Qualification
119
+ uses optimized apps on an Android API 35 arm64 emulator and an iOS 26.5 arm64
120
+ simulator with Debug/local native SDK transport, including expiry after the app
121
+ process is suspended, source mapping and encrypted retry after relaunch.
122
+ Physical devices, device sleep and production transport are not qualified by
123
+ these checks. Matching the SDK's broader RN peer range does not establish
124
+ rejection support. Other versions, JSC, browser execution, tvOS,
125
+ unverified Promise replacements and incompatible hooks return `unsupported`.
126
+ The adapter checks runtime identity and a fulfilled-only hook handshake; it
127
+ does not generate a test rejection or replace the Promise constructor.
128
+
129
+ Rejections still unhandled after 2 seconds enter the existing nonfatal capture
130
+ path. A handler attached within that interval cancels capture; later handling
131
+ does not retract an accepted report. The notification uses a JavaScript timer.
132
+ Android pauses JavaScript timers while the app's activity is paused (in the
133
+ background, behind another activity or in picture-in-picture), although
134
+ JavaScript can keep running. Rejections observed then are notified on later
135
+ Promise activity or when the activity resumes. Explicit capture and automatic
136
+ reporting share accepted error identity, and automatic reports retain their
137
+ existing 10-distinct-key allowance per mount. Automatic reports are
138
+ deduplicated by exception type and top stack frame. Non-Error reasons have no
139
+ stack and are deduplicated by their reported value with digit runs ignored, so
140
+ `Request 1001 failed` repeats `Request 1000 failed` and all object reasons
141
+ share one report per mount. They spend at most 5 of the 10 keys, so values that
142
+ differ in other ways, such as hex identifiers, still leave 5 keys for `Error`
143
+ rejections and other automatic reports.
144
+
145
+ The observer retains at most 16 detached snapshots, each at most 64 KiB of
146
+ serialized UTF-8 data. It drops new arrivals when full and oversize snapshots
147
+ rather than retaining arbitrary error graphs. Work is counted as expired
148
+ instead of reported when it is notified more than 30 seconds after the
149
+ rejection. Age is measured on the platform's elapsed clock and bounded by
150
+ wall-clock time, which keeps advancing while an Android device sleeps. Cleanup
151
+ cannot run while JavaScript is suspended, so stale work expires when
152
+ JavaScript resumes. Error facts and causes are snapshotted at rejection time.
153
+ Rejection reasons that are not `Error` objects are reported as `UnhandledValue`
154
+ without a stack or cause chain, even when they carry a `cause`. Strings,
155
+ numbers, booleans, `null` and `undefined` keep their bounded value; objects,
156
+ arrays, functions, symbols and bigints are reported only as a type label such
157
+ as `[object]` rather than serialized.
158
+ Native context and breadcrumbs are collected at notification time. A preserved
159
+ external tracker may independently retain errors or generate its own reports.
160
+
161
+ `getPromiseRejectionStatus()` returns `disabled`, `unsupported`, `observing`,
162
+ `displaced`, `install-failed`, or `not-mounted`, with a reason and bounded
163
+ counters for pending, accepted, cancelled, dropped, expired, suppressed,
164
+ refused, failed and discarded captures. A rejection handled within the 2-second interval counts as cancelled,
165
+ even if it was dropped or failed to snapshot on arrival. Counters saturate at
166
+ 2,147,483,647 and reset on each mount; they contain no captured messages or
167
+ stacks. Replacing either hook stops this observer until a new mount and
168
+ releases its pending work, counted as `discarded`.
169
+ `crashReporting.disabled: true` overrides the opt-in, and unmount discards
170
+ pending work. Configuration changes take effect on a new mount.
171
+ The browser export reports Hermes observation as unsupported and preserves the
172
+ browser SDK's own error handling.
173
+
174
+ ## Error capture and diagnostics
175
+
176
+ ### React error boundary callback
177
+
178
+ An existing React class boundary can opt into handled capture. Render the
179
+ boundary inside `EverframeProvider`:
180
+
181
+ ```tsx
182
+ import * as React from 'react';
183
+ import { EverframeProvider } from '@everframe/react-native';
184
+ import { captureReactError } from '@everframe/react-native/integrations/react';
185
+
186
+ class Boundary extends React.Component<React.PropsWithChildren, { failed: boolean }> {
187
+ state = { failed: false };
188
+ static getDerivedStateFromError() { return { failed: true }; }
189
+ componentDidCatch(error: unknown, info: React.ErrorInfo) {
190
+ captureReactError(error, info);
191
+ }
192
+ render() { return this.state.failed ? null : this.props.children; }
193
+ }
194
+
195
+ export default function App() {
196
+ return (
197
+ <EverframeProvider config={{ apiKey: '…' }}>
198
+ <Boundary>
199
+ <RootNavigator />
200
+ </Boundary>
201
+ </EverframeProvider>
202
+ );
203
+ }
204
+ ```
205
+
206
+ A boundary that wraps `EverframeProvider` does not report through it: its
207
+ fallback replaces the provider in the same commit, before `componentDidCatch`
208
+ runs. Such a boundary reports only if its fallback renders its own
209
+ `EverframeProvider`, which mounts before `componentDidCatch` runs. Errors that a
210
+ boundary inside the provider catches during the provider's first commit (the
211
+ initial render that also mounts the provider) are not captured, and they are
212
+ not replayed. Disabled crash reporting and calls after unmount are also inert.
213
+ In development builds, a call that finds no mounted provider logs a warning, at
214
+ most once until a provider mounts or unmounts.
215
+
216
+ The application owns fallback and recovery UI. This callback reports caught
217
+ component errors; it does not install global renderer hooks or capture arbitrary
218
+ async/event-handler errors. Component-stack metadata is bounded and redacted by
219
+ the existing capture pipeline, without replacing exception frames or cause data.
220
+
221
+ The original error shares accepted identity with manual and nonfatal automatic
222
+ capture within a mount. Native refusal permits recapture; equal fingerprint keys
223
+ can also suppress distinct objects. A later fatal escalation may report separately.
224
+ The adapter subpath is unavailable to browser-conditioned consumers.
225
+
226
+ ### Local capture admission diagnostics
227
+
228
+ ```ts
229
+ import { getErrorCaptureStatus } from '@everframe/react-native';
230
+ const status = getErrorCaptureStatus();
231
+ const { attempted, accepted, duplicateSuppressed } = status.counters.handled;
232
+ ```
233
+
234
+ Snapshots are detached, contain no captured content, reset on remount, and perform
235
+ no native query or upload. Paths are `handled` (including the boundary callback),
236
+ `errorUtils`, and `rejection` (submission only). Each active attempt has one outcome:
237
+ `accepted`, `duplicateSuppressed`, `allowanceSuppressed`, `bridgeUnavailable`,
238
+ `nativeRefused`, `captureFailed`, `reentrantSuppressed`, `inactiveAborted`, or
239
+ `legacyAttempted`. Counters saturate at 2,147,483,647. Existing limits are ten
240
+ handled and ten automatic distinct keys; fatal capture bypasses them.
241
+
242
+ `accepted` requires exactly true from a current native capture method. It does
243
+ not mean upload success, server processing or outbox removal. An automatic call
244
+ to an older native fallback is only `legacyAttempted`, regardless of its return.
245
+ Old-binary unavailable/throwing methods are covered by unit controls, not an
246
+ installed old-binary qualification. This snapshot excludes native queue and
247
+ delivery observations; read those with `getReportDeliveryStatus()`. Status is
248
+ neutral `not-mounted` without a provider, `disabled` under the crash-reporting
249
+ veto, and `unsupported` on the browser root entry.
250
+
251
+ Installed boundary qualification passes on Android API 35 arm64 emulator and
252
+ iOS 26.5 arm64 simulator for this exact phone-host row:
253
+ React 19.2.5, `react-native-tvos` 0.85.3-0 aliased as `react-native`, Expo
254
+ 56.0.0-preview.7, Hermes 250829098.0.10 / bytecode 98. Other RN distributions,
255
+ versions, TV, physical devices and production endpoints are not qualified by
256
+ this matrix. Installed checks cover four genuine boundary capture cases,
257
+ disabled/initial zero controls, mapped causes, and byte-identical encrypted retry
258
+ after relaunch. The application/JS builds are optimized; native SDKs are
259
+ Debug/local. Physical-device and production-endpoint qualification remains open.
260
+
261
+ ### Native report delivery diagnostics
262
+
263
+ `getReportDeliveryStatus()` synchronously copies cached native observations. It
264
+ performs no file reads, uploads, drains, or hook installation. Use
265
+ `getErrorCaptureStatus()` separately for JavaScript admission decisions.
266
+
267
+ ```ts
268
+ import { getReportDeliveryStatus } from '@everframe/react-native';
269
+ const status = getReportDeliveryStatus();
270
+ console.log(status.status, status.queue.observation, status.queue.pendingCount);
271
+ ```
272
+
273
+ The schema separates native capture acceptance, queue operations and settled
274
+ transport attempts (`live-submit` and `outbox-drain`). `server-accepted` means an
275
+ HTTP 2xx response; it does not establish processing, symbolication, dashboard
276
+ visibility or queue removal. Removal has its own operation counter. Counters are
277
+ operations, not unique reports, and saturate at 2,147,483,647.
278
+
279
+ The queue is the SDK's shared report outbox, including reporter submissions and
280
+ entries from earlier configurations. Its count is last-observed, not a fresh
281
+ measurement or a current-project/crash-only total. Missing counts mean unknown;
282
+ `partial` means only readable entries were counted. Android rejects new entries
283
+ at capacity and retains terminal HTTP responses during drain. iOS can evict older
284
+ entries and attempts removal after terminal HTTP responses. Custom standalone
285
+ outbox/submitter instances are not automatically included.
286
+
287
+ Snapshots contain only fixed codes and counters, never report content, IDs,
288
+ URLs, credentials or exception strings. Observations are process-local and reset
289
+ on native start/reconfiguration or kill. They are best-effort: contention and
290
+ termination may lose observations; a busy getter reports `snapshot-busy`.
291
+ `bridge-handled` and `bridge-automatic` cover fact entry paths shared by framework
292
+ bridges. iOS marks `jvm-uncaught` unsupported and implies no automatic iOS native
293
+ crash collector.
294
+
295
+ Without a mounted provider the result is `not-mounted/no-mount`; the browser
296
+ export is `unsupported/platform`. Older native SDKs without the getter return
297
+ `unsupported/native-method-missing`. A throwing native call returns
298
+ `unavailable/native-call-failed`, and malformed native responses return
299
+ `unavailable/invalid-native-snapshot`. These fallbacks have an unobserved queue
300
+ and `unknown` platform policies. This API does not provide a per-report delivery
301
+ guarantee or trigger a retry.
302
+
79
303
  ## Network body capture (client veto)
80
304
 
81
305
  ```tsx
@@ -500,6 +724,40 @@ recipes above.
500
724
 
501
725
  ---
502
726
 
727
+ ## Android reporter initialization
728
+
729
+ The Android reporter can be opened after the host activity has resumed, including
730
+ apps that disable AndroidX Startup. Use matching updated React Native bridge and
731
+ `dev.everframe:reporter-ui` artifacts: the bridge supplies the current resumed
732
+ activity, and installation and presentation run on the main thread. Repeated
733
+ configuration with matching updated artifacts does not register another lifecycle observer. Capture configuration
734
+ remains synchronous without waiting for reporter UI initialization.
735
+
736
+ The existing `ReporterResolverInstaller.create(Context)` entry point is retained
737
+ for compiled callers. A new bridge with an older reporter uses its legacy entry
738
+ point when the activity-aware method is absent; that combination retains the old
739
+ late-initialization limitation. A failure from an available method is reported,
740
+ not retried through the legacy path. Background or unavailable activities return
741
+ a cancelled result with reason `no_active_activity`.
742
+
743
+ Legacy off-main `create(Context)` schedules installation and returns before it is
744
+ ready; use the awaited activity-aware bridge to open immediately after configure.
745
+ With a new bridge and an older reporter, each configure, companion start, and
746
+ open invokes legacy installation again. This can add duplicate lifecycle
747
+ callbacks and overwrite that older reporter's resolver and activity supplier;
748
+ the previous bridge limited installation to once per module. Conversely, an old
749
+ compiled bridge with the new reporter can announce companion PIN capability
750
+ before asynchronous reporter installation finishes, temporarily advertising no
751
+ PIN support. Use the matching updated pair for the qualified behavior.
752
+
753
+ Activity destruction while a reporter is open resolves the pending call with
754
+ `{ status: "cancelled", reason: "activity_destroyed" }`, releases that report's
755
+ frozen replay capture, and clears presenting state so the reporter can open
756
+ again. Backgrounding alone does not cancel the dialog. Once Send is tapped,
757
+ submission owns the result and continues independently of activity destruction;
758
+ its normal delivery result is preserved. Unsaved drafts are not restored after
759
+ activity recreation.
760
+
503
761
  ## License
504
762
 
505
763
  MIT
@@ -152,7 +152,7 @@ val nativeVersionOverride = (findProperty("everframeNativeVersion") as String?)
152
152
  val rnSdkPkg = groovy.json.JsonSlurper()
153
153
  .parse(file("../package.json")) as Map<*, *>
154
154
  val rnSdkVersion = rnSdkPkg["version"] as String
155
- val nativeMinorRange: String = nativeVersionOverride ?: "[0.9.0,0.10.0)"
155
+ val nativeMinorRange: String = nativeVersionOverride ?: "[1.1.0,1.2.0)"
156
156
  if (nativeVersionOverride != null) {
157
157
  logger.lifecycle(
158
158
  "[everframe-rn] native AAR pinned to $nativeVersionOverride via everframeNativeVersion " +
@@ -105,6 +105,8 @@ class EverframeModule(
105
105
  private val reactContext: ReactApplicationContext,
106
106
  ) : NativeEverframeSpec(reactContext) {
107
107
 
108
+ private val reporterBridge = ReporterBridge()
109
+
108
110
  private val videoPrivacyAdapter = Everframe.__registerVideoPrivacyAdapter(RnVideoPrivacyAdapter)
109
111
  private val sensitiveRegistration = RnSensitiveRegistration(
110
112
  { tag -> UIManagerHelper.getUIManagerForReactTag(reactContext, tag)?.resolveView(tag) },
@@ -331,7 +333,7 @@ class EverframeModule(
331
333
  "Everframe.rn",
332
334
  "configure: SDK already running this exact config — skipping start()",
333
335
  )
334
- installPresenterResolverIfNeeded()
336
+ reporterBridge.scheduleInstall(reactContext)
335
337
  completed = true
336
338
  return@txGuardSurface
337
339
  }
@@ -357,10 +359,10 @@ class EverframeModule(
357
359
 
358
360
  // Wire TXReporterPresenter as the resolver for
359
361
  // Everframe.shared.report.open(). Mirrors iOS TXReporterPresenter
360
- // .installResolver(). Reflective lookup keeps :sdk-react-native
361
- // from a hard dependency on :everframe-reporter-ui (which the host
362
- // app pulls in via its app build.gradle).
363
- installPresenterResolverIfNeeded()
362
+ // .installResolver(). Reflection preserves compatibility with
363
+ // separately built reporter artifacts; the awaited open retries
364
+ // installation on main before selecting the resumed host.
365
+ reporterBridge.scheduleInstall(reactContext)
364
366
  completed = true
365
367
  }
366
368
  return completed
@@ -382,16 +384,9 @@ class EverframeModule(
382
384
  promise.reject(REJECT_CAPTURE_DISABLED, "Everframe not started (captureGate=false). Call configure() first.")
383
385
  return
384
386
  }
385
- // ReporterResolverInstaller tracks the active Activity via its own
386
- // ActivityRegistry — we don't pass currentActivity through. But we
387
- // ensure the resolver is installed so report.open() doesn't throw.
388
- installPresenterResolverIfNeeded()
389
-
390
387
  MainScope().launch {
391
388
  try {
392
- val result: ReportResult = withContext(Dispatchers.Main) {
393
- Everframe.report.open()
394
- }
389
+ val result: ReportResult = reporterBridge.open(reactContext)
395
390
  val dict = Arguments.createMap().apply {
396
391
  when (result) {
397
392
  is ReportResult.Submitted -> {
@@ -410,7 +405,8 @@ class EverframeModule(
410
405
  }
411
406
  promise.resolve(dict)
412
407
  } catch (t: Throwable) {
413
- promise.reject(REJECT_OPEN_REPORTER_FAILED, t.message, t)
408
+ promise.reject(REJECT_OPEN_REPORTER_FAILED, reporterBridge.rejectionMessage(t))
409
+ if (t is kotlinx.coroutines.CancellationException) throw t
414
410
  }
415
411
  }
416
412
  }
@@ -641,6 +637,10 @@ class EverframeModule(
641
637
 
642
638
  // ---------------- synchronous crash persistence ----------------
643
639
 
640
+ /** Copies cached native facts; never initializes storage or starts a drain. */
641
+ @ReactMethod(isBlockingSynchronousMethod = true)
642
+ override fun getReportDeliveryStatusJson(): String = Everframe.getReportDeliveryStatus().toJson()
643
+
644
644
  /** Automatic ErrorUtils capture; true acknowledges completed sidecar storage. */
645
645
  @ReactMethod(isBlockingSynchronousMethod = true)
646
646
  override fun reportCrash(crashJson: String): Boolean = captureCrashJson(crashJson, handled = false)
@@ -670,15 +670,16 @@ class EverframeModule(
670
670
  }
671
671
  }.getOrNull()
672
672
  val details = if (obj.has("details")) obj.get("details") else null
673
+ val causeChain = if (obj.has("causeChain")) obj.get("causeChain") else null
673
674
  val fatal = !handled && obj.optBoolean("fatal", false)
674
675
  val accepted = if (handled) {
675
- dev.everframe.crash.CrashReporter.captureHandledFactsWithDetails(
676
- exceptionType, message, frames, occurredAt, jsBundle, details,
676
+ dev.everframe.crash.CrashReporter.captureHandledFactsWithCauses(
677
+ exceptionType, message, frames, occurredAt, jsBundle, details, causeChain, "everframe-android",
677
678
  )
678
679
  } else {
679
- dev.everframe.crash.CrashReporter.captureFactsAcceptedWithDetails(
680
+ dev.everframe.crash.CrashReporter.captureFactsAcceptedWithCauses(
680
681
  exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
681
- fatal, occurredAt, jsBundle, details,
682
+ fatal, occurredAt, jsBundle, details, causeChain,
682
683
  )
683
684
  }
684
685
  // Scheduling is best-effort after acceptance; a scheduler failure
@@ -851,6 +852,10 @@ class EverframeModule(
851
852
 
852
853
  @ReactMethod
853
854
  override fun startCompanion() {
855
+ if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
856
+ android.os.Handler(android.os.Looper.getMainLooper()).post { startCompanion() }
857
+ return
858
+ }
854
859
  txGuardVoid("startCompanion") {
855
860
  // Idempotent: a live client means a prior startCompanion already
856
861
  // opened the socket. Do NOT recreate it — callers must
@@ -864,13 +869,16 @@ class EverframeModule(
864
869
  return@txGuardVoid
865
870
  }
866
871
 
872
+ // Await the main-thread install before the relay can announce PIN capability.
873
+ // configureSync itself remains non-blocking with respect to main.
874
+ reporterBridge.scheduleInstall(reactContext)
867
875
  installCompanionCollectorsIfNeeded()
868
876
 
869
877
  // Runtime suppression of the built-in PIN presenter (spec
870
878
  // 2026-08-19, controller ruling): a runtime flag, not an
871
879
  // install-site gate. `:everframe-reporter-ui`'s
872
880
  // `CompanionPinPresenter` is auto-installed via
873
- // `ReporterResolverInstaller` (androidx.startup) regardless of
881
+ // `ReporterResolverInstaller` (Startup or the bridge above) regardless of
874
882
  // the configured mode, so `Everframe.__attachPinUiInstalled`
875
883
  // stays an honest capability signal — only the PRESENTING is
876
884
  // silenced, here, at the one site where the mode is already
@@ -976,6 +984,10 @@ class EverframeModule(
976
984
 
977
985
  @ReactMethod
978
986
  override fun stopCompanion() {
987
+ if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
988
+ android.os.Handler(android.os.Looper.getMainLooper()).post { stopCompanion() }
989
+ return
990
+ }
979
991
  txGuardVoid("stopCompanion") {
980
992
  // External review, finding NN1 — only ever stop/unregister OUR
981
993
  // OWN client. `companionClient` is non-null here ONLY when a
@@ -1607,34 +1619,7 @@ class EverframeModule(
1607
1619
 
1608
1620
  // ---------------- Helpers ----------------
1609
1621
 
1610
- @Volatile
1611
- private var resolverInstalled = false
1612
1622
 
1613
- /**
1614
- * Idempotently runs `ReporterResolverInstaller.create(applicationContext)`
1615
- * — the androidx.startup `Initializer` from :everframe-reporter-ui. Hosts
1616
- * using AndroidX App Startup will have this run automatically at process
1617
- * launch; this method covers hosts that disable App Startup (common in
1618
- * React Native apps) by invoking the same code path reflectively. No-op
1619
- * after the first successful call.
1620
- */
1621
- private fun installPresenterResolverIfNeeded() {
1622
- if (resolverInstalled) return
1623
- try {
1624
- val cls = Class.forName("dev.everframe.ui.ReporterResolverInstaller")
1625
- val ctor = cls.getDeclaredConstructor()
1626
- ctor.isAccessible = true
1627
- val instance = ctor.newInstance()
1628
- val create = cls.getMethod("create", android.content.Context::class.java)
1629
- create.invoke(instance, reactContext.applicationContext)
1630
- resolverInstalled = true
1631
- } catch (t: Throwable) {
1632
- android.util.Log.w(
1633
- "Everframe.rn",
1634
- "ReporterResolverInstaller not on classpath; openReporter will fail until the host app depends on :everframe-reporter-ui (${t.javaClass.simpleName}: ${t.message})",
1635
- )
1636
- }
1637
- }
1638
1623
 
1639
1624
  private fun ReadableMap.takeIfHasString(key: String): String? =
1640
1625
  if (hasKey(key) && !isNull(key)) getString(key) else null
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // SPDX-FileCopyrightText: 2026 ScriptX
3
+ package dev.everframe.rn
4
+
5
+ import android.app.Activity
6
+ import android.content.Context
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.util.Log
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.facebook.react.common.LifecycleState
12
+ import dev.everframe.Everframe
13
+ import dev.everframe.config.ReportResult
14
+ import java.lang.reflect.InvocationTargetException
15
+ import java.lang.reflect.Method
16
+ import kotlin.coroutines.Continuation
17
+ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
18
+ import kotlinx.coroutines.CancellationException
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.withContext
21
+
22
+ /** Reflection preserves compatibility with separately built native reporter artifacts. */
23
+ internal class ReporterBridge(
24
+ private val resolveInstaller: () -> Class<*> = { Class.forName("dev.everframe.ui.ReporterResolverInstaller") },
25
+ ) {
26
+ fun scheduleInstall(context: Context) {
27
+ val application = context.applicationContext
28
+ val install = Runnable {
29
+ try {
30
+ val type = installerType()
31
+ invoke(type.getMethod("create", Context::class.java), type.getDeclaredConstructor().newInstance(), application)
32
+ } catch (_: Throwable) {
33
+ Log.w("Everframe.rn", "Reporter initialization deferred; opening will retry")
34
+ }
35
+ }
36
+ if (Looper.myLooper() == Looper.getMainLooper()) install.run()
37
+ else Handler(Looper.getMainLooper()).post(install)
38
+ }
39
+
40
+ suspend fun open(context: ReactApplicationContext): ReportResult = withContext(Dispatchers.Main.immediate) {
41
+ val activity = context.currentActivity
42
+ if (context.lifecycleState != LifecycleState.RESUMED || activity == null ||
43
+ activity.isFinishing || activity.isDestroyed || activity.application !== context.applicationContext) {
44
+ return@withContext ReportResult.Cancelled("no_active_activity")
45
+ }
46
+ try {
47
+ val type = installerType()
48
+ 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
+ }
60
+ }
61
+ } catch (failure: Throwable) {
62
+ when (failure) {
63
+ is CancellationException -> throw failure
64
+ is ReporterBridgeFailure -> throw failure
65
+ is NoSuchMethodException -> throw ReporterBridgeFailure("Reporter UI is incompatible; update the native reporter dependency")
66
+ else -> throw ReporterBridgeFailure("Reporter initialization or opening failed")
67
+ }
68
+ }
69
+ }
70
+
71
+ private fun installerType(): Class<*> = try { resolveInstaller() }
72
+ catch (_: ClassNotFoundException) {
73
+ throw ReporterBridgeFailure("Reporter UI dependency is missing; add dev.everframe:reporter-ui")
74
+ }
75
+
76
+ private fun invoke(method: Method, instance: Any, vararg arguments: Any?): Any? =
77
+ try { method.invoke(instance, *arguments) }
78
+ catch (failure: InvocationTargetException) { throw failure.targetException }
79
+
80
+ /** Only SDK-owned constant messages may cross the JS promise boundary. */
81
+ fun rejectionMessage(failure: Throwable): String = when (failure) {
82
+ is ReporterBridgeFailure -> failure.message!!
83
+ is CancellationException -> "Reporter opening was cancelled"
84
+ else -> "Reporter initialization or opening failed"
85
+ }
86
+
87
+ // No host exception/cause is forwarded to JS or diagnostic logs.
88
+ private class ReporterBridgeFailure(message: String) : IllegalStateException(message)
89
+ }
@@ -228,6 +228,8 @@ interface Spec extends TurboModule {
228
228
  * freeform-object escape hatch; `from`/`to` win over its keys.
229
229
  */
230
230
  recordScreen(name: string, data?: UnsafeObject): void;
231
+ /** Additive diagnostic bridge: bounded JSON of cached native observations; no I/O. */
232
+ getReportDeliveryStatusJson(): string;
231
233
  /**
232
234
  * Crash/error reporting (spec 2026-07-18). SYNCHRONOUS by design (non-void
233
235
  * return forces a sync TurboModule method): the fatal path must complete
@@ -0,0 +1 @@
1
+ import{a as o}from"./chunk-XUSV3KOD.js";var h=["native-handled","bridge-handled","bridge-automatic","jvm-uncaught"],A=["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"],K=["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]/,k=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(A,()=>0)}))},queue:{scope:"sdk-report-outbox",observation:"not-observed",quality:"unknown",capacityPolicy:"unknown",terminalHttpPolicy:"unknown",operations:d(S,()=>0),migration:"not-observed"},transport:d(R,()=>({settledAttempts:0,outcomes:d(K,()=>0)}))}}o(J,"emptyReportDeliveryStatus");function l(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 i=n[a];if(!g(t,i)&&!g(r,i))throw new Error}return e}o(l,"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 c(e,t){if(typeof e!="string"||!g(t,e))throw new Error;return e}o(c,"enumeration");function v(e){if(typeof e!="number"||!U(e)||e<0||e>k)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=l(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>k?k:a)!==n)throw new Error;let i=y(e,"lastOutcome")?c(e.lastOutcome,t):void 0;if(n>0!=(i!==void 0)||i!==void 0&&r[i]===0)throw new Error;return{settledAttempts:n,outcomes:r,...i===void 0?{}:{lastOutcome:i}}}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=l(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=c(t.status,["active","not-started","disabled","unavailable"]),n=c(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=l(t.capture,["enabled","paths"]),i=l(a.paths,h),b=d(h,p=>{let f=l(i[p],["supported","settledAttempts","outcomes"],["lastOutcome"]),m={supported:P(f.supported),...j(f,A)};if(!m.supported&&m.settledAttempts!==0)throw new Error;return m}),u=l(t.queue,["scope","observation","quality","capacityPolicy","terminalHttpPolicy","operations","migration"],["pendingCount","lastFailure"]);if(u.scope!=="sdk-report-outbox")return null;let s={scope:"sdk-report-outbox",observation:c(u.observation,["not-observed","observed","failed"]),quality:c(u.quality,["complete","partial","unknown"]),capacityPolicy:c(u.capacityPolicy,["reject-new","evict-oldest"]),terminalHttpPolicy:c(u.terminalHttpPolicy,["retain","attempt-remove"]),operations:q(u.operations,S),migration:c(u.migration,["not-observed","clear","blocked","unknown"]),...y(u,"pendingCount")?{pendingCount:v(u.pendingCount)}:{},...y(u,"lastFailure")?{lastFailure:c(u.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=l(t.transport,R),E=d(R,p=>{let f=l(D[p],["settledAttempts","outcomes"],["lastOutcome","lastHttpStatus"]),m=j(f,K);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 O(){return{attempted:0,accepted:0,duplicateSuppressed:0,allowanceSuppressed:0,bridgeUnavailable:0,nativeRefused:0,captureFailed:0,reentrantSuppressed:0,inactiveAborted:0,legacyAttempted:0}}o(O,"emptyCounters");function N(e,t){return{status:e,reason:t,scope:"mounted-js-admission",limits:{handled:10,automatic:10},counters:{handled:O(),errorUtils:O(),rejection:O()}}}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};
@@ -0,0 +1 @@
1
+ import{a as i,c as s,e as u}from"./chunk-ENPQ66EV.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.")}};var r=null,a={};function x(){return a}t(x,"__getContextEpoch");function f(){try{return r?.getPromiseRejectionStatus?.()??u("not-mounted","no-mount")}catch{return u("install-failed","hook-install")}}t(f,"getPromiseRejectionStatus");function E(e){r=e,a={}}t(E,"__setCurrentContext");function g(){return r}t(g,"__getCurrentContext");function R(){let e=r;return e?e.open():Promise.reject(new n("EverframeProvider not mounted; wrap your root with <EverframeProvider>."))}t(R,"open");function S(e){r?.setExtra(e)}t(S,"setExtra");function y(e){r?.addBreadcrumb(e)}t(y,"addBreadcrumb");function C(e,o){o!==void 0?r?.recordScreen(e,o):r?.recordScreen(e)}t(C,"recordScreen");function b(e){r?.setUser(e)}t(b,"setUser");function j(e,o){r?.captureException(e,o)}t(j,"captureException");function w(){try{return r?.getErrorCaptureStatus?.()??s("not-mounted","no-mount")}catch{return s("not-mounted","no-mount")}}t(w,"getErrorCaptureStatus");function P(){let e=r;try{let o=e?.getReportDeliveryStatus?.();return e===r&&o?o:i("not-mounted","no-mount")}catch{return i("unavailable","native-call-failed")}}t(P,"getReportDeliveryStatus");export{n as a,x as b,f as c,E as d,g as e,R as f,S as g,y as h,C as i,b as j,j as k,w as l,P as m};