@everframe/react-native 1.0.0 → 1.2.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 +5 -1
- package/README.md +295 -7
- package/android/build.gradle.kts +7 -6
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +80 -49
- package/android/src/main/java/dev/everframe/rn/ReporterBridge.kt +89 -0
- package/dist/NativeEverframe.d.ts +26 -1
- package/dist/chunk-ENPQ66EV.js +1 -0
- package/dist/chunk-POE5CSO5.js +1 -0
- package/dist/index.d.ts +451 -67
- package/dist/index.js +31 -25
- package/dist/index.web.d.ts +12 -0
- package/dist/index.web.js +1 -1
- 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 +58 -56
- package/ios/Sources/EverframeModule.mm +21 -4
- package/package.json +9 -4
- package/src/NativeEverframe.ts +28 -2
- package/dist/chunk-QRPINMCR.js +0 -1
package/EverframeRN.podspec
CHANGED
|
@@ -35,8 +35,12 @@ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
|
35
35
|
# evaluated from inside the published npm tarball at a consumer's install, where
|
|
36
36
|
# no workspace path exists. Unset — every consumer install — the derivation
|
|
37
37
|
# below is the only source of truth, unchanged.
|
|
38
|
+
#
|
|
39
|
+
# The bridge calls `EverframeConfig(sdkKey:)`, which first ships in native
|
|
40
|
+
# 1.2.0, so the range already asks for 1.2 while package.json is still 1.1.0.
|
|
41
|
+
# The 1.2.0 release moves package.json to match.
|
|
38
42
|
native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
|
|
39
|
-
native_minor_range = native_version_override.empty? ? '~> 1.
|
|
43
|
+
native_minor_range = native_version_override.empty? ? '~> 1.2.0' : native_version_override
|
|
40
44
|
|
|
41
45
|
Pod::Spec.new do |s|
|
|
42
46
|
s.name = 'EverframeRN'
|
package/README.md
CHANGED
|
@@ -11,6 +11,9 @@ modal to RN host apps via a TurboModule + a thin React provider/hook API.
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
See the [crash-reporting support matrix](../../docs/crash-reporting-support.md)
|
|
15
|
+
for exact tested paths, unsupported combinations and remaining qualification.
|
|
16
|
+
|
|
14
17
|
## Installation
|
|
15
18
|
|
|
16
19
|
```bash
|
|
@@ -56,11 +59,11 @@ import { EverframeProvider, useEverframe } from '@everframe/react-native';
|
|
|
56
59
|
|
|
57
60
|
export default function App() {
|
|
58
61
|
return (
|
|
59
|
-
// `
|
|
62
|
+
// `sdkKey` is the only required field. The ingest endpoint is a
|
|
60
63
|
// compile-time constant in the SDK and is not configurable in v1.
|
|
61
64
|
<EverframeProvider
|
|
62
65
|
config={{
|
|
63
|
-
|
|
66
|
+
sdkKey: 'evf_live_xxxxxxxxxxxxxxxx',
|
|
64
67
|
// Recommended in RN/Expo development: shake also opens the dev menu.
|
|
65
68
|
shakeToReport: { enabled: !__DEV__ },
|
|
66
69
|
}}
|
|
@@ -96,10 +99,259 @@ provider context and attach any non-sensitive context needed for diagnosis.
|
|
|
96
99
|
The source API requires matching rebuilt native components; published `0.7.0`
|
|
97
100
|
artifacts are not evidence of support.
|
|
98
101
|
|
|
102
|
+
## Release health
|
|
103
|
+
|
|
104
|
+
Release health counts crash-free foreground sessions for each version of your
|
|
105
|
+
app. It is on by default, and debug builds of the native app aren't counted.
|
|
106
|
+
Turn it off in the `EverframeProvider` config:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
releaseHealth: { enabled: false },
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Sessions come from the Android and iOS SDKs and count toward the native app
|
|
113
|
+
version, which an over-the-air update doesn't change. Release health needs the
|
|
114
|
+
Hermes bundle identity that `@everframe/metro` adds at build time (or the
|
|
115
|
+
`jsBundle` option); without it, release health stays off and the SDK logs an
|
|
116
|
+
`[everframe]` warning (in development builds only, unless you set
|
|
117
|
+
`releaseHealth`). Add `userId: accountId` to count crash-free users, and
|
|
118
|
+
`includeDebugBuilds: true` to count debug builds too. The configuration is read
|
|
119
|
+
when the Provider mounts, so remount it with a changed `key` to change `enabled`
|
|
120
|
+
or `userId`. A configure with `enabled: false` turns release health off and
|
|
121
|
+
erases its queued records on the device.
|
|
122
|
+
Release health skips a debug build, which is `__DEV__` unless you set
|
|
123
|
+
`debugBuild` on the Provider. See
|
|
124
|
+
[release health](../../docs/release-health.md).
|
|
125
|
+
|
|
126
|
+
## Promise rejection observation
|
|
127
|
+
|
|
128
|
+
Automatic Hermes rejection observation is opt-in:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { EverframeProvider, getPromiseRejectionStatus } from '@everframe/react-native';
|
|
132
|
+
|
|
133
|
+
<EverframeProvider config={{
|
|
134
|
+
sdkKey: '…',
|
|
135
|
+
crashReporting: { promiseRejections: { enabled: true } },
|
|
136
|
+
}}>
|
|
137
|
+
<App />
|
|
138
|
+
</EverframeProvider>
|
|
139
|
+
|
|
140
|
+
// Read after mount. This reports observer coverage, not delivery confirmation.
|
|
141
|
+
const status = getPromiseRejectionStatus();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The verified runtime is Android/iOS `react-native-tvos@0.85.3-0` with Hermes
|
|
145
|
+
release `250829098.0.10`, Release/Static Hermes and bytecode 98. Qualification
|
|
146
|
+
uses optimized apps on an Android API 35 arm64 emulator and an iOS 26.5 arm64
|
|
147
|
+
simulator with Debug/local native SDK transport, including expiry after the app
|
|
148
|
+
process is suspended, source mapping and encrypted retry after relaunch.
|
|
149
|
+
Physical devices, device sleep and production transport are not qualified by
|
|
150
|
+
these checks. Matching the SDK's broader RN peer range does not establish
|
|
151
|
+
rejection support. Other versions, JSC, browser execution, tvOS,
|
|
152
|
+
unverified Promise replacements and incompatible hooks return `unsupported`.
|
|
153
|
+
The adapter checks runtime identity and a fulfilled-only hook handshake; it
|
|
154
|
+
does not generate a test rejection or replace the Promise constructor.
|
|
155
|
+
|
|
156
|
+
Rejections still unhandled after 2 seconds enter the existing nonfatal capture
|
|
157
|
+
path. A handler attached within that interval cancels capture; later handling
|
|
158
|
+
does not retract an accepted report. The notification uses a JavaScript timer.
|
|
159
|
+
Android pauses JavaScript timers while the app's activity is paused (in the
|
|
160
|
+
background, behind another activity or in picture-in-picture), although
|
|
161
|
+
JavaScript can keep running. Rejections observed then are notified on later
|
|
162
|
+
Promise activity or when the activity resumes. Explicit capture and automatic
|
|
163
|
+
reporting share accepted error identity, and automatic reports retain their
|
|
164
|
+
existing 10-distinct-key allowance per mount. Automatic reports are
|
|
165
|
+
deduplicated by exception type and top stack frame. Non-Error reasons have no
|
|
166
|
+
stack and are deduplicated by their reported value with digit runs ignored, so
|
|
167
|
+
`Request 1001 failed` repeats `Request 1000 failed` and all object reasons
|
|
168
|
+
share one report per mount. They spend at most 5 of the 10 keys, so values that
|
|
169
|
+
differ in other ways, such as hex identifiers, still leave 5 keys for `Error`
|
|
170
|
+
rejections and other automatic reports.
|
|
171
|
+
|
|
172
|
+
The observer retains at most 16 detached snapshots, each at most 64 KiB of
|
|
173
|
+
serialized UTF-8 data. It drops new arrivals when full and oversize snapshots
|
|
174
|
+
rather than retaining arbitrary error graphs. Work is counted as expired
|
|
175
|
+
instead of reported when it is notified more than 30 seconds after the
|
|
176
|
+
rejection. Age is measured on the platform's elapsed clock and bounded by
|
|
177
|
+
wall-clock time, which keeps advancing while an Android device sleeps. Cleanup
|
|
178
|
+
cannot run while JavaScript is suspended, so stale work expires when
|
|
179
|
+
JavaScript resumes. Error facts and causes are snapshotted at rejection time.
|
|
180
|
+
Rejection reasons that are not `Error` objects are reported as `UnhandledValue`
|
|
181
|
+
without a stack or cause chain, even when they carry a `cause`. Strings,
|
|
182
|
+
numbers, booleans, `null` and `undefined` keep their bounded value; objects,
|
|
183
|
+
arrays, functions, symbols and bigints are reported only as a type label such
|
|
184
|
+
as `[object]` rather than serialized.
|
|
185
|
+
Native context and breadcrumbs are collected at notification time. A preserved
|
|
186
|
+
external tracker may independently retain errors or generate its own reports.
|
|
187
|
+
|
|
188
|
+
`getPromiseRejectionStatus()` returns `disabled`, `unsupported`, `observing`,
|
|
189
|
+
`displaced`, `install-failed`, or `not-mounted`, with a reason and bounded
|
|
190
|
+
counters for pending, accepted, cancelled, dropped, expired, suppressed,
|
|
191
|
+
refused, failed and discarded captures. A rejection handled within the 2-second interval counts as cancelled,
|
|
192
|
+
even if it was dropped or failed to snapshot on arrival. Counters saturate at
|
|
193
|
+
2,147,483,647 and reset on each mount; they contain no captured messages or
|
|
194
|
+
stacks. Replacing either hook stops this observer until a new mount and
|
|
195
|
+
releases its pending work, counted as `discarded`.
|
|
196
|
+
`crashReporting.disabled: true` overrides the opt-in, and unmount discards
|
|
197
|
+
pending work. Configuration changes take effect on a new mount.
|
|
198
|
+
The browser export reports Hermes observation as unsupported and preserves the
|
|
199
|
+
browser SDK's own error handling.
|
|
200
|
+
|
|
201
|
+
## Error capture and diagnostics
|
|
202
|
+
|
|
203
|
+
### Native crashes and ANRs
|
|
204
|
+
|
|
205
|
+
Crash reporting also turns on the native SDKs' automatic crash capture. On Android that covers JVM exceptions, native crashes (OS exit records on API 30+, with tombstone frames on API 31+) and ANRs on API 30+; see the [Android SDK guide](../sdk-android/README.md#crash-capture-on-by-default). On iOS it is the native crash recorder. `crashReporting: { disabled: true }` turns all of it off on both platforms.
|
|
206
|
+
|
|
207
|
+
### Errors no boundary catches
|
|
208
|
+
|
|
209
|
+
Since React Native 0.78, React reports an error that no error boundary catches
|
|
210
|
+
(thrown while rendering, in an effect or in a lifecycle method) through the
|
|
211
|
+
root's `onUncaughtError`. That calls React Native's `ExceptionsManager`, never
|
|
212
|
+
the `ErrorUtils` handler. Crash reporting hooks
|
|
213
|
+
`ExceptionsManager.handleException` too, so such an error is reported as a fatal
|
|
214
|
+
JavaScript crash (`mechanism: "errorutils"`) with its Hermes stack, and React's
|
|
215
|
+
component stack follows the message after `This error is located at:`, as in
|
|
216
|
+
React Native's own report. React unmounts the whole tree, `EverframeProvider`
|
|
217
|
+
included, before it reports the error; the hook stays until the end of that
|
|
218
|
+
task, so the error is still captured. The native fatal React Native raises for
|
|
219
|
+
the same error (`RCTFatalException` on iOS and tvOS, `JavascriptException` on
|
|
220
|
+
Android) is not reported a second time. An error in the render that first
|
|
221
|
+
mounts the provider comes before this capture is installed. Errors a boundary
|
|
222
|
+
catches are not reported automatically; use the callback below.
|
|
223
|
+
|
|
224
|
+
### React error boundary callback
|
|
225
|
+
|
|
226
|
+
An existing React class boundary can opt into handled capture. Render the
|
|
227
|
+
boundary inside `EverframeProvider`:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import * as React from 'react';
|
|
231
|
+
import { EverframeProvider } from '@everframe/react-native';
|
|
232
|
+
import { captureReactError } from '@everframe/react-native/integrations/react';
|
|
233
|
+
|
|
234
|
+
class Boundary extends React.Component<React.PropsWithChildren, { failed: boolean }> {
|
|
235
|
+
state = { failed: false };
|
|
236
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
237
|
+
componentDidCatch(error: unknown, info: React.ErrorInfo) {
|
|
238
|
+
captureReactError(error, info);
|
|
239
|
+
}
|
|
240
|
+
render() { return this.state.failed ? null : this.props.children; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default function App() {
|
|
244
|
+
return (
|
|
245
|
+
<EverframeProvider config={{ sdkKey: '…' }}>
|
|
246
|
+
<Boundary>
|
|
247
|
+
<RootNavigator />
|
|
248
|
+
</Boundary>
|
|
249
|
+
</EverframeProvider>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A boundary that wraps `EverframeProvider` does not report through it: its
|
|
255
|
+
fallback replaces the provider in the same commit, before `componentDidCatch`
|
|
256
|
+
runs. Such a boundary reports only if its fallback renders its own
|
|
257
|
+
`EverframeProvider`, which mounts before `componentDidCatch` runs. Errors that a
|
|
258
|
+
boundary inside the provider catches during the provider's first commit (the
|
|
259
|
+
initial render that also mounts the provider) are not captured, and they are
|
|
260
|
+
not replayed. Disabled crash reporting and calls after unmount are also inert.
|
|
261
|
+
In development builds, a call that finds no mounted provider logs a warning, at
|
|
262
|
+
most once until a provider mounts or unmounts.
|
|
263
|
+
|
|
264
|
+
The application owns fallback and recovery UI. This callback reports caught
|
|
265
|
+
component errors; it does not install global renderer hooks or capture arbitrary
|
|
266
|
+
async/event-handler errors. Component-stack metadata is bounded and redacted by
|
|
267
|
+
the existing capture pipeline, without replacing exception frames or cause data.
|
|
268
|
+
|
|
269
|
+
The original error shares accepted identity with manual and nonfatal automatic
|
|
270
|
+
capture within a mount. Native refusal permits recapture; equal fingerprint keys
|
|
271
|
+
can also suppress distinct objects. A later fatal escalation may report separately.
|
|
272
|
+
The adapter subpath is unavailable to browser-conditioned consumers.
|
|
273
|
+
|
|
274
|
+
### Local capture admission diagnostics
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { getErrorCaptureStatus } from '@everframe/react-native';
|
|
278
|
+
const status = getErrorCaptureStatus();
|
|
279
|
+
const { attempted, accepted, duplicateSuppressed } = status.counters.handled;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Snapshots are detached, contain no captured content, reset on remount, and perform
|
|
283
|
+
no native query or upload. Paths are `handled` (including the boundary callback),
|
|
284
|
+
`errorUtils` (including errors no boundary catches), and `rejection` (submission only). Each active attempt has one outcome:
|
|
285
|
+
`accepted`, `duplicateSuppressed`, `allowanceSuppressed`, `bridgeUnavailable`,
|
|
286
|
+
`nativeRefused`, `captureFailed`, `reentrantSuppressed`, `inactiveAborted`, or
|
|
287
|
+
`legacyAttempted`. Counters saturate at 2,147,483,647. Existing limits are ten
|
|
288
|
+
handled and ten automatic distinct keys; fatal capture bypasses them.
|
|
289
|
+
|
|
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
|
|
295
|
+
delivery observations; read those with `getReportDeliveryStatus()`. Status is
|
|
296
|
+
neutral `not-mounted` without a provider, `disabled` under the crash-reporting
|
|
297
|
+
veto, and `unsupported` on the browser root entry.
|
|
298
|
+
|
|
299
|
+
Installed boundary qualification passes on Android API 35 arm64 emulator and
|
|
300
|
+
iOS 26.5 arm64 simulator for this exact phone-host row:
|
|
301
|
+
React 19.2.5, `react-native-tvos` 0.85.3-0 aliased as `react-native`, Expo
|
|
302
|
+
56.0.0-preview.7, Hermes 250829098.0.10 / bytecode 98. Other RN distributions,
|
|
303
|
+
versions, TV, physical devices and production endpoints are not qualified by
|
|
304
|
+
this matrix. Installed checks cover four genuine boundary capture cases,
|
|
305
|
+
disabled/initial zero controls, mapped causes, and byte-identical encrypted retry
|
|
306
|
+
after relaunch. The application/JS builds are optimized; native SDKs are
|
|
307
|
+
Debug/local. Physical-device and production-endpoint qualification remains open.
|
|
308
|
+
|
|
309
|
+
### Native report delivery diagnostics
|
|
310
|
+
|
|
311
|
+
`getReportDeliveryStatus()` synchronously copies cached native observations. It
|
|
312
|
+
performs no file reads, uploads, drains, or hook installation. Use
|
|
313
|
+
`getErrorCaptureStatus()` separately for JavaScript admission decisions.
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import { getReportDeliveryStatus } from '@everframe/react-native';
|
|
317
|
+
const status = getReportDeliveryStatus();
|
|
318
|
+
console.log(status.status, status.queue.observation, status.queue.pendingCount);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The schema separates native capture acceptance, queue operations and settled
|
|
322
|
+
transport attempts (`live-submit` and `outbox-drain`). `server-accepted` means an
|
|
323
|
+
HTTP 2xx response; it does not establish processing, symbolication, dashboard
|
|
324
|
+
visibility or queue removal. Removal has its own operation counter. Counters are
|
|
325
|
+
operations, not unique reports, and saturate at 2,147,483,647.
|
|
326
|
+
|
|
327
|
+
The queue is the SDK's shared report outbox, including reporter submissions and
|
|
328
|
+
entries from earlier configurations. Its count is last-observed, not a fresh
|
|
329
|
+
measurement or a current-project/crash-only total. Missing counts mean unknown;
|
|
330
|
+
`partial` means only readable entries were counted. Android rejects new entries
|
|
331
|
+
at capacity and retains terminal HTTP responses during drain. iOS can evict older
|
|
332
|
+
entries and attempts removal after terminal HTTP responses. Custom standalone
|
|
333
|
+
outbox/submitter instances are not automatically included.
|
|
334
|
+
|
|
335
|
+
Snapshots contain only fixed codes and counters, never report content, IDs,
|
|
336
|
+
URLs, credentials or exception strings. Observations are process-local and reset
|
|
337
|
+
on native start/reconfiguration or kill. They are best-effort: contention and
|
|
338
|
+
termination may lose observations; a busy getter reports `snapshot-busy`.
|
|
339
|
+
`bridge-handled` and `bridge-automatic` cover fact entry paths shared by framework
|
|
340
|
+
bridges. iOS marks `jvm-uncaught` unsupported and implies no automatic iOS native
|
|
341
|
+
crash collector.
|
|
342
|
+
|
|
343
|
+
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
|
|
346
|
+
`unavailable/native-call-failed`, and malformed native responses return
|
|
347
|
+
`unavailable/invalid-native-snapshot`. These fallbacks have an unobserved queue
|
|
348
|
+
and `unknown` platform policies. This API does not provide a per-report delivery
|
|
349
|
+
guarantee or trigger a retry.
|
|
350
|
+
|
|
99
351
|
## Network body capture (client veto)
|
|
100
352
|
|
|
101
353
|
```tsx
|
|
102
|
-
<EverframeProvider config={{
|
|
354
|
+
<EverframeProvider config={{ sdkKey: '…', networkBodies: { disabled: true } }}>
|
|
103
355
|
```
|
|
104
356
|
|
|
105
357
|
`networkBodies.disabled: true` is a **client veto** — it can only turn body
|
|
@@ -182,7 +434,7 @@ import { createNavigationContainerRef } from '@react-navigation/native';
|
|
|
182
434
|
const navigationRef = createNavigationContainerRef();
|
|
183
435
|
const txNav = reactNavigationIntegration({ navigationRef });
|
|
184
436
|
|
|
185
|
-
<EverframeProvider config={{
|
|
437
|
+
<EverframeProvider config={{ sdkKey, integrations: [consoleIntegration(), txNav] }}>
|
|
186
438
|
<NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
|
|
187
439
|
...
|
|
188
440
|
```
|
|
@@ -211,7 +463,7 @@ inside a native view and never reaches JS.
|
|
|
211
463
|
### Config
|
|
212
464
|
|
|
213
465
|
```tsx
|
|
214
|
-
<EverframeProvider config={{
|
|
466
|
+
<EverframeProvider config={{ sdkKey, vitals: { enabled: true, sampleRate: 0.5, captureSourceQuery: false } }}>
|
|
215
467
|
```
|
|
216
468
|
|
|
217
469
|
- `enabled` — absent follows the dashboard toggle; `false` opts out locally;
|
|
@@ -222,7 +474,9 @@ inside a native view and never reaches JS.
|
|
|
222
474
|
|
|
223
475
|
**Reconfiguring.** A Provider remount with the *same* config does not restart
|
|
224
476
|
the SDK: both native sides compare the incoming config against the *installed*
|
|
225
|
-
one and skip the start entirely. A **changed** config does restart it
|
|
477
|
+
one and skip the start entirely. A **changed** config does restart it (on iOS,
|
|
478
|
+
except a change confined to `releaseHealth` or the loaded bundle; see
|
|
479
|
+
[Release health](#release-health)) — and a
|
|
226
480
|
start supersedes the running SDK, detaching every player integration it had
|
|
227
481
|
announced. **Limitation:** players tracked before that restart stay untracked
|
|
228
482
|
until their screens remount; nothing re-registers them automatically. Two React
|
|
@@ -394,7 +648,7 @@ on Android), so React Native's focus engine never sees it — the native VC
|
|
|
394
648
|
manages focus on its own UIWindow / Activity.
|
|
395
649
|
|
|
396
650
|
```tsx
|
|
397
|
-
<EverframeProvider config={{
|
|
651
|
+
<EverframeProvider config={{ sdkKey }}>
|
|
398
652
|
<NavigationContainer>{/* focus engine root */}</NavigationContainer>
|
|
399
653
|
</EverframeProvider>
|
|
400
654
|
```
|
|
@@ -520,6 +774,40 @@ recipes above.
|
|
|
520
774
|
|
|
521
775
|
---
|
|
522
776
|
|
|
777
|
+
## Android reporter initialization
|
|
778
|
+
|
|
779
|
+
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
|
|
782
|
+
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.
|
|
802
|
+
|
|
803
|
+
Activity destruction while a reporter is open resolves the pending call with
|
|
804
|
+
`{ status: "cancelled", reason: "activity_destroyed" }`, releases that report's
|
|
805
|
+
frozen replay capture, and clears presenting state so the reporter can open
|
|
806
|
+
again. Backgrounding alone does not cancel the dialog. Once Send is tapped,
|
|
807
|
+
submission owns the result and continues independently of activity destruction;
|
|
808
|
+
its normal delivery result is preserved. Unsaved drafts are not restored after
|
|
809
|
+
activity recreation.
|
|
810
|
+
|
|
523
811
|
## License
|
|
524
812
|
|
|
525
813
|
MIT
|
package/android/build.gradle.kts
CHANGED
|
@@ -21,11 +21,10 @@
|
|
|
21
21
|
// spec); a host on the new architecture can flip the parent class with a
|
|
22
22
|
// 1-line patch once codegen output is available in the consuming build.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
// No `<uses-permission>` in AndroidManifest.xml. The bridge adds
|
|
26
|
-
// permissions on top of
|
|
27
|
-
//
|
|
28
|
-
// acceptance grep.
|
|
24
|
+
// PERMISSIONS:
|
|
25
|
+
// No `<uses-permission>` in AndroidManifest.xml. The bridge adds no
|
|
26
|
+
// permissions on top of what `:everframe-core` declares (INTERNET only,
|
|
27
|
+
// merged into the host app's manifest).
|
|
29
28
|
//
|
|
30
29
|
// SCOPE (NOT in this build):
|
|
31
30
|
// • Namespace is `dev.everframe.rn`.
|
|
@@ -152,7 +151,9 @@ val nativeVersionOverride = (findProperty("everframeNativeVersion") as String?)
|
|
|
152
151
|
val rnSdkPkg = groovy.json.JsonSlurper()
|
|
153
152
|
.parse(file("../package.json")) as Map<*, *>
|
|
154
153
|
val rnSdkVersion = rnSdkPkg["version"] as String
|
|
155
|
-
|
|
154
|
+
// Native 1.2.0 is the release that renamed the SDK key to `sdkKey`; the
|
|
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
157
|
if (nativeVersionOverride != null) {
|
|
157
158
|
logger.lifecycle(
|
|
158
159
|
"[everframe-rn] native AAR pinned to $nativeVersionOverride via everframeNativeVersion " +
|
|
@@ -76,6 +76,8 @@ import dev.everframe.companion.CompanionSubmissionComposer
|
|
|
76
76
|
import dev.everframe.companion.PreviewCapture
|
|
77
77
|
import dev.everframe.companion.RelayWSClient
|
|
78
78
|
import dev.everframe.companion.parseCompanionBadgePosition
|
|
79
|
+
import dev.everframe.config.ReleaseHealthConfig
|
|
80
|
+
import dev.everframe.config.ReleaseHealthBundleStatus
|
|
79
81
|
import dev.everframe.config.CaptureConfig
|
|
80
82
|
import dev.everframe.config.Environment
|
|
81
83
|
import dev.everframe.config.ReportResult
|
|
@@ -105,6 +107,8 @@ class EverframeModule(
|
|
|
105
107
|
private val reactContext: ReactApplicationContext,
|
|
106
108
|
) : NativeEverframeSpec(reactContext) {
|
|
107
109
|
|
|
110
|
+
private val reporterBridge = ReporterBridge()
|
|
111
|
+
|
|
108
112
|
private val videoPrivacyAdapter = Everframe.__registerVideoPrivacyAdapter(RnVideoPrivacyAdapter)
|
|
109
113
|
private val sensitiveRegistration = RnSensitiveRegistration(
|
|
110
114
|
{ tag -> UIManagerHelper.getUIManagerForReactTag(reactContext, tag)?.resolveView(tag) },
|
|
@@ -213,12 +217,45 @@ class EverframeModule(
|
|
|
213
217
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
214
218
|
override fun configureSync(opts: ReadableMap): Boolean = configureInternal(opts)
|
|
215
219
|
|
|
220
|
+
private fun parseReleaseHealth(opts: ReadableMap): ReleaseHealthConfig? {
|
|
221
|
+
// Invalid bridge input revokes earlier monitoring instead of retaining an old subject.
|
|
222
|
+
// The SDK reads the app version itself; the bridge carries the loaded bundle, the user ID and
|
|
223
|
+
// whether debug builds count. Release health is on by default: a missing or null flag is on, and
|
|
224
|
+
// only an explicit false is off. It still needs a valid loaded bundle, so no id means off.
|
|
225
|
+
return runCatching {
|
|
226
|
+
if (opts.takeIfHasBoolean("releaseHealthEnabled") == false) return null
|
|
227
|
+
val loaded = opts.takeIfHasString("releaseHealthLoadedBuildId") ?: return null
|
|
228
|
+
val user = opts.takeIfHasString("releaseHealthUserId")
|
|
229
|
+
if (!validHealthText(loaded, 200) || (user != null && !validHealthText(user, 128))) return null
|
|
230
|
+
val includeDebugBuilds = opts.takeIfHasBoolean("releaseHealthIncludeDebugBuilds") ?: false
|
|
231
|
+
ReleaseHealthConfig(enabled = true, userId = user, loadedBuildId = loaded,
|
|
232
|
+
loadedBundleStatus = ReleaseHealthBundleStatus.KNOWN, includeDebugBuilds = includeDebugBuilds)
|
|
233
|
+
}.getOrNull()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private fun validHealthText(value: String, maximum: Int): Boolean {
|
|
237
|
+
// Match the wire's UTF-16 limit and ECMAScript blank definition, preserving ID bytes.
|
|
238
|
+
fun blank(c: Char) = c in '\u0009'..'\u000d' || c == ' ' || c == '\u00a0' ||
|
|
239
|
+
c == '\u1680' || c in '\u2000'..'\u200a' || c == '\u2028' || c == '\u2029' ||
|
|
240
|
+
c == '\u202f' || c == '\u205f' || c == '\u3000' || c == '\ufeff'
|
|
241
|
+
if (value.length > maximum || value.all(::blank)) return false
|
|
242
|
+
var index = 0
|
|
243
|
+
while (index < value.length) {
|
|
244
|
+
val c = value[index++]
|
|
245
|
+
if (c.code < 32 || Character.isLowSurrogate(c)) return false
|
|
246
|
+
if (Character.isHighSurrogate(c)) {
|
|
247
|
+
if (index >= value.length || !Character.isLowSurrogate(value[index++])) return false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
216
253
|
private fun configureInternal(opts: ReadableMap): Boolean {
|
|
217
254
|
var completed = false
|
|
218
255
|
txGuardSurface("configure") {
|
|
219
|
-
val sdkKey = opts.takeIfHasString("
|
|
256
|
+
val sdkKey = opts.takeIfHasString("sdkKey")
|
|
220
257
|
?: throw JSApplicationIllegalArgumentException(
|
|
221
|
-
"configure: required field '
|
|
258
|
+
"configure: required field 'sdkKey' missing",
|
|
222
259
|
)
|
|
223
260
|
val sdkVersion = opts.takeIfHasString("sdkVersion") ?: Everframe.SDK_VERSION
|
|
224
261
|
|
|
@@ -231,6 +268,9 @@ class EverframeModule(
|
|
|
231
268
|
// Polarity flips exactly once, here: JS sends the veto
|
|
232
269
|
// (`installIdentifierDisabled`), native stores the permission.
|
|
233
270
|
val installIdentifierDisabled = opts.takeIfHasBoolean("installIdentifierDisabled") ?: false
|
|
271
|
+
// Polarity flips exactly once, here: JS sends the veto
|
|
272
|
+
// (`crashReportingDisabled`), native stores the permission.
|
|
273
|
+
val crashReportingDisabled = opts.takeIfHasBoolean("crashReportingDisabled") ?: false
|
|
234
274
|
// Attach-PIN UI mode (spec 2026-08-19) — a flat optional STRING
|
|
235
275
|
// field (codegen cannot express a string-literal union), parsed
|
|
236
276
|
// here and stashed for `startCompanion()` to read (the flag is a
|
|
@@ -243,12 +283,20 @@ class EverframeModule(
|
|
|
243
283
|
val attachPinUi = parseAttachPinUi(opts.takeIfHasString("attachPinUi"))
|
|
244
284
|
|
|
245
285
|
val config = EverframeConfig(
|
|
286
|
+
// The JS config carries no App ID, and the native SDK requires a
|
|
287
|
+
// non-blank one. It only keys this app's low-memory-kill issue, so
|
|
288
|
+
// the SDK key stands in, as it always has; changing it would split
|
|
289
|
+
// existing React Native low-memory issues.
|
|
246
290
|
appId = sdkKey,
|
|
247
291
|
sdkKey = sdkKey,
|
|
248
292
|
environment = Environment.production,
|
|
249
293
|
release = sdkVersion,
|
|
250
|
-
capture = CaptureConfig(networkBodies = !networkBodiesDisabled),
|
|
294
|
+
capture = CaptureConfig(networkBodies = !networkBodiesDisabled, crash = !crashReportingDisabled),
|
|
251
295
|
installIdentifierEnabled = !installIdentifierDisabled,
|
|
296
|
+
releaseHealth = parseReleaseHealth(opts),
|
|
297
|
+
// The JS facade always sends React Native's `__DEV__` (or the host's override). A missing
|
|
298
|
+
// or null key stays null so the native SDK decides for itself, as for any other caller.
|
|
299
|
+
debugBuild = opts.takeIfHasBoolean("debugBuild"),
|
|
252
300
|
// Explicit companion device-identity override (naming spec
|
|
253
301
|
// 2026-08-24) — read straight off the flat ConfigOpts field,
|
|
254
302
|
// same shape as attachPinUi above. Absent/blank falls
|
|
@@ -331,7 +379,7 @@ class EverframeModule(
|
|
|
331
379
|
"Everframe.rn",
|
|
332
380
|
"configure: SDK already running this exact config — skipping start()",
|
|
333
381
|
)
|
|
334
|
-
|
|
382
|
+
reporterBridge.scheduleInstall(reactContext)
|
|
335
383
|
completed = true
|
|
336
384
|
return@txGuardSurface
|
|
337
385
|
}
|
|
@@ -357,10 +405,10 @@ class EverframeModule(
|
|
|
357
405
|
|
|
358
406
|
// Wire TXReporterPresenter as the resolver for
|
|
359
407
|
// Everframe.shared.report.open(). Mirrors iOS TXReporterPresenter
|
|
360
|
-
// .installResolver().
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
408
|
+
// .installResolver(). Reflection preserves compatibility with
|
|
409
|
+
// separately built reporter artifacts; the awaited open retries
|
|
410
|
+
// installation on main before selecting the resumed host.
|
|
411
|
+
reporterBridge.scheduleInstall(reactContext)
|
|
364
412
|
completed = true
|
|
365
413
|
}
|
|
366
414
|
return completed
|
|
@@ -382,16 +430,9 @@ class EverframeModule(
|
|
|
382
430
|
promise.reject(REJECT_CAPTURE_DISABLED, "Everframe not started (captureGate=false). Call configure() first.")
|
|
383
431
|
return
|
|
384
432
|
}
|
|
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
433
|
MainScope().launch {
|
|
391
434
|
try {
|
|
392
|
-
val result: ReportResult =
|
|
393
|
-
Everframe.report.open()
|
|
394
|
-
}
|
|
435
|
+
val result: ReportResult = reporterBridge.open(reactContext)
|
|
395
436
|
val dict = Arguments.createMap().apply {
|
|
396
437
|
when (result) {
|
|
397
438
|
is ReportResult.Submitted -> {
|
|
@@ -410,7 +451,8 @@ class EverframeModule(
|
|
|
410
451
|
}
|
|
411
452
|
promise.resolve(dict)
|
|
412
453
|
} catch (t: Throwable) {
|
|
413
|
-
promise.reject(REJECT_OPEN_REPORTER_FAILED,
|
|
454
|
+
promise.reject(REJECT_OPEN_REPORTER_FAILED, reporterBridge.rejectionMessage(t))
|
|
455
|
+
if (t is kotlinx.coroutines.CancellationException) throw t
|
|
414
456
|
}
|
|
415
457
|
}
|
|
416
458
|
}
|
|
@@ -641,6 +683,10 @@ class EverframeModule(
|
|
|
641
683
|
|
|
642
684
|
// ---------------- synchronous crash persistence ----------------
|
|
643
685
|
|
|
686
|
+
/** Copies cached native facts; never initializes storage or starts a drain. */
|
|
687
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
688
|
+
override fun getReportDeliveryStatusJson(): String = Everframe.getReportDeliveryStatus().toJson()
|
|
689
|
+
|
|
644
690
|
/** Automatic ErrorUtils capture; true acknowledges completed sidecar storage. */
|
|
645
691
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
646
692
|
override fun reportCrash(crashJson: String): Boolean = captureCrashJson(crashJson, handled = false)
|
|
@@ -670,15 +716,16 @@ class EverframeModule(
|
|
|
670
716
|
}
|
|
671
717
|
}.getOrNull()
|
|
672
718
|
val details = if (obj.has("details")) obj.get("details") else null
|
|
719
|
+
val causeChain = if (obj.has("causeChain")) obj.get("causeChain") else null
|
|
673
720
|
val fatal = !handled && obj.optBoolean("fatal", false)
|
|
674
721
|
val accepted = if (handled) {
|
|
675
|
-
dev.everframe.crash.CrashReporter.
|
|
676
|
-
exceptionType, message, frames, occurredAt, jsBundle, details,
|
|
722
|
+
dev.everframe.crash.CrashReporter.captureHandledFactsWithCauses(
|
|
723
|
+
exceptionType, message, frames, occurredAt, jsBundle, details, causeChain, "everframe-android",
|
|
677
724
|
)
|
|
678
725
|
} else {
|
|
679
|
-
dev.everframe.crash.CrashReporter.
|
|
726
|
+
dev.everframe.crash.CrashReporter.captureFactsAcceptedWithCauses(
|
|
680
727
|
exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
|
|
681
|
-
fatal, occurredAt, jsBundle, details,
|
|
728
|
+
fatal, occurredAt, jsBundle, details, causeChain,
|
|
682
729
|
)
|
|
683
730
|
}
|
|
684
731
|
// Scheduling is best-effort after acceptance; a scheduler failure
|
|
@@ -851,6 +898,10 @@ class EverframeModule(
|
|
|
851
898
|
|
|
852
899
|
@ReactMethod
|
|
853
900
|
override fun startCompanion() {
|
|
901
|
+
if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
|
|
902
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post { startCompanion() }
|
|
903
|
+
return
|
|
904
|
+
}
|
|
854
905
|
txGuardVoid("startCompanion") {
|
|
855
906
|
// Idempotent: a live client means a prior startCompanion already
|
|
856
907
|
// opened the socket. Do NOT recreate it — callers must
|
|
@@ -864,13 +915,16 @@ class EverframeModule(
|
|
|
864
915
|
return@txGuardVoid
|
|
865
916
|
}
|
|
866
917
|
|
|
918
|
+
// Await the main-thread install before the relay can announce PIN capability.
|
|
919
|
+
// configureSync itself remains non-blocking with respect to main.
|
|
920
|
+
reporterBridge.scheduleInstall(reactContext)
|
|
867
921
|
installCompanionCollectorsIfNeeded()
|
|
868
922
|
|
|
869
923
|
// Runtime suppression of the built-in PIN presenter (spec
|
|
870
924
|
// 2026-08-19, controller ruling): a runtime flag, not an
|
|
871
925
|
// install-site gate. `:everframe-reporter-ui`'s
|
|
872
926
|
// `CompanionPinPresenter` is auto-installed via
|
|
873
|
-
// `ReporterResolverInstaller` (
|
|
927
|
+
// `ReporterResolverInstaller` (Startup or the bridge above) regardless of
|
|
874
928
|
// the configured mode, so `Everframe.__attachPinUiInstalled`
|
|
875
929
|
// stays an honest capability signal — only the PRESENTING is
|
|
876
930
|
// silenced, here, at the one site where the mode is already
|
|
@@ -976,6 +1030,10 @@ class EverframeModule(
|
|
|
976
1030
|
|
|
977
1031
|
@ReactMethod
|
|
978
1032
|
override fun stopCompanion() {
|
|
1033
|
+
if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
|
|
1034
|
+
android.os.Handler(android.os.Looper.getMainLooper()).post { stopCompanion() }
|
|
1035
|
+
return
|
|
1036
|
+
}
|
|
979
1037
|
txGuardVoid("stopCompanion") {
|
|
980
1038
|
// External review, finding NN1 — only ever stop/unregister OUR
|
|
981
1039
|
// OWN client. `companionClient` is non-null here ONLY when a
|
|
@@ -1607,34 +1665,7 @@ class EverframeModule(
|
|
|
1607
1665
|
|
|
1608
1666
|
// ---------------- Helpers ----------------
|
|
1609
1667
|
|
|
1610
|
-
@Volatile
|
|
1611
|
-
private var resolverInstalled = false
|
|
1612
1668
|
|
|
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
1669
|
|
|
1639
1670
|
private fun ReadableMap.takeIfHasString(key: String): String? =
|
|
1640
1671
|
if (hasKey(key) && !isNull(key)) getString(key) else null
|