@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.
- package/EverframeRN.podspec +1 -1
- package/README.md +258 -0
- package/android/build.gradle.kts +1 -1
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +31 -46
- package/android/src/main/java/dev/everframe/rn/ReporterBridge.kt +89 -0
- package/dist/NativeEverframe.d.ts +2 -0
- package/dist/chunk-ENPQ66EV.js +1 -0
- package/dist/chunk-POE5CSO5.js +1 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +25 -25
- package/dist/index.web.d.ts +13 -0
- package/dist/index.web.js +1 -0
- package/dist/integrations/console.js +1 -1
- package/dist/integrations/react-navigation.js +1 -1
- package/dist/integrations/react.d.ts +15 -0
- package/dist/integrations/react.js +1 -0
- package/dist/promise-rejection-types-C-lxCdDl.d.ts +101 -0
- package/ios/Sources/EverframeBridge.swift +5 -0
- package/ios/Sources/EverframeModule.mm +4 -0
- package/package.json +19 -4
- package/src/NativeEverframe.ts +3 -0
- package/dist/chunk-QRPINMCR.js +0 -1
package/EverframeRN.podspec
CHANGED
|
@@ -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? ? '~>
|
|
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
|
package/android/build.gradle.kts
CHANGED
|
@@ -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 ?: "[
|
|
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
|
-
|
|
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().
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
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 =
|
|
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,
|
|
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.
|
|
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.
|
|
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` (
|
|
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};
|