@everframe/react-native 1.1.0 → 1.2.1
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 +119 -40
- package/android/build.gradle.kts +7 -6
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +86 -39
- package/android/src/main/java/dev/everframe/rn/ReporterBridge.kt +4 -13
- package/dist/NativeEverframe.d.ts +44 -6
- package/dist/browser-4GHZL7XI.js +1 -0
- package/dist/chunk-MIWWEGLO.js +1 -0
- package/dist/chunk-UN3Q4UGZ.js +1 -0
- package/dist/index.d.ts +353 -23
- package/dist/index.js +31 -25
- package/dist/index.web.d.ts +2 -2
- 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.js +1 -1
- package/dist/{promise-rejection-types-C-lxCdDl.d.ts → promise-rejection-types-Bf5iLv8n.d.ts} +1 -3
- package/ios/Sources/EverframeBridge.swift +105 -69
- package/ios/Sources/EverframeEventEmitter.swift +3 -5
- package/ios/Sources/EverframeModule.mm +34 -20
- package/package.json +4 -4
- package/src/NativeEverframe.ts +47 -9
- package/dist/browser-ZQAY6B4M.js +0 -1
- package/dist/chunk-ENPQ66EV.js +0 -1
- package/dist/chunk-POE5CSO5.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.1
|
|
43
|
+
native_minor_range = native_version_override.empty? ? '~> 1.2.1' : 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
|
|
@@ -26,6 +29,25 @@ Then install pods (iOS) / sync Gradle (Android) as usual:
|
|
|
26
29
|
cd ios && pod install
|
|
27
30
|
```
|
|
28
31
|
|
|
32
|
+
### Source maps and symbols
|
|
33
|
+
|
|
34
|
+
Release builds can upload their Hermes source maps, dSYMs, R8 mappings and
|
|
35
|
+
native libraries, so crashes show your code. Wrap Metro with
|
|
36
|
+
`withEverframe` from `@everframe/metro`, add `@everframe/cli`, and set up the
|
|
37
|
+
native projects once. In Expo, apply `withEverframe` from `@everframe/expo`
|
|
38
|
+
with your App ID in `app.config.js`. In a bare app, run:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @everframe/cli setup react-native --app-id "$EVERFRAME_APP_ID"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Either way, Android gets `apply plugin: "dev.everframe"` and
|
|
45
|
+
`everframe { appId = "…" }` plus one classpath line, and Xcode gets a Run
|
|
46
|
+
Script phase that runs `everframe xcode build-phase`. The upload logic lives in
|
|
47
|
+
the Gradle plugin and the CLI. Set `EVERFRAME_API_TOKEN` in release builds;
|
|
48
|
+
without it the build warns and continues. See the
|
|
49
|
+
[CLI guide](../cli/README.md#react-native-and-expo).
|
|
50
|
+
|
|
29
51
|
## React Native Web
|
|
30
52
|
|
|
31
53
|
`@everframe/react-native` has a browser entry and an explicit
|
|
@@ -46,6 +68,8 @@ the web mask registry. Native iOS and Android use their native capture. A web
|
|
|
46
68
|
bundler that honors the `browser` export condition can keep the regular package
|
|
47
69
|
import; use the `/web` subpath if its resolver does not select that condition.
|
|
48
70
|
|
|
71
|
+
Replays also attach to errors; see [session replay](../../docs/session-replay.md).
|
|
72
|
+
|
|
49
73
|
## Usage
|
|
50
74
|
|
|
51
75
|
Wrap your app with `<EverframeProvider>` at the highest practical level
|
|
@@ -56,11 +80,11 @@ import { EverframeProvider, useEverframe } from '@everframe/react-native';
|
|
|
56
80
|
|
|
57
81
|
export default function App() {
|
|
58
82
|
return (
|
|
59
|
-
// `
|
|
83
|
+
// `sdkKey` is the only required field. The ingest endpoint is a
|
|
60
84
|
// compile-time constant in the SDK and is not configurable in v1.
|
|
61
85
|
<EverframeProvider
|
|
62
86
|
config={{
|
|
63
|
-
|
|
87
|
+
sdkKey: 'evf_live_xxxxxxxxxxxxxxxx',
|
|
64
88
|
// Recommended in RN/Expo development: shake also opens the dev menu.
|
|
65
89
|
shakeToReport: { enabled: !__DEV__ },
|
|
66
90
|
}}
|
|
@@ -96,6 +120,30 @@ provider context and attach any non-sensitive context needed for diagnosis.
|
|
|
96
120
|
The source API requires matching rebuilt native components; published `0.7.0`
|
|
97
121
|
artifacts are not evidence of support.
|
|
98
122
|
|
|
123
|
+
## Release health
|
|
124
|
+
|
|
125
|
+
Release health counts crash-free foreground sessions for each version of your
|
|
126
|
+
app. It is on by default, and debug builds of the native app aren't counted.
|
|
127
|
+
Turn it off in the `EverframeProvider` config:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
releaseHealth: { enabled: false },
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Sessions come from the Android and iOS SDKs and count toward the native app
|
|
134
|
+
version, which an over-the-air update doesn't change. Release health needs the
|
|
135
|
+
Hermes bundle identity that `@everframe/metro` adds at build time (or the
|
|
136
|
+
`jsBundle` option); without it, release health stays off and the SDK logs an
|
|
137
|
+
`[everframe]` warning (in development builds only, unless you set
|
|
138
|
+
`releaseHealth`). Add `userId: accountId` to count crash-free users, and
|
|
139
|
+
`includeDebugBuilds: true` to count debug builds too. The configuration is read
|
|
140
|
+
when the Provider mounts, so remount it with a changed `key` to change `enabled`
|
|
141
|
+
or `userId`. A configure with `enabled: false` turns release health off and
|
|
142
|
+
erases its queued records on the device.
|
|
143
|
+
Release health skips a debug build, which is `__DEV__` unless you set
|
|
144
|
+
`debugBuild` on the Provider. See
|
|
145
|
+
[release health](../../docs/release-health.md).
|
|
146
|
+
|
|
99
147
|
## Promise rejection observation
|
|
100
148
|
|
|
101
149
|
Automatic Hermes rejection observation is opt-in:
|
|
@@ -104,7 +152,7 @@ Automatic Hermes rejection observation is opt-in:
|
|
|
104
152
|
import { EverframeProvider, getPromiseRejectionStatus } from '@everframe/react-native';
|
|
105
153
|
|
|
106
154
|
<EverframeProvider config={{
|
|
107
|
-
|
|
155
|
+
sdkKey: '…',
|
|
108
156
|
crashReporting: { promiseRejections: { enabled: true } },
|
|
109
157
|
}}>
|
|
110
158
|
<App />
|
|
@@ -173,6 +221,27 @@ browser SDK's own error handling.
|
|
|
173
221
|
|
|
174
222
|
## Error capture and diagnostics
|
|
175
223
|
|
|
224
|
+
### Native crashes and ANRs
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
### Errors no boundary catches
|
|
229
|
+
|
|
230
|
+
Since React Native 0.78, React reports an error that no error boundary catches
|
|
231
|
+
(thrown while rendering, in an effect or in a lifecycle method) through the
|
|
232
|
+
root's `onUncaughtError`. That calls React Native's `ExceptionsManager`, never
|
|
233
|
+
the `ErrorUtils` handler. Crash reporting hooks
|
|
234
|
+
`ExceptionsManager.handleException` too, so such an error is reported as a fatal
|
|
235
|
+
JavaScript crash (`mechanism: "errorutils"`) with its Hermes stack, and React's
|
|
236
|
+
component stack follows the message after `This error is located at:`, as in
|
|
237
|
+
React Native's own report. React unmounts the whole tree, `EverframeProvider`
|
|
238
|
+
included, before it reports the error; the hook stays until the end of that
|
|
239
|
+
task, so the error is still captured. The native fatal React Native raises for
|
|
240
|
+
the same error (`RCTFatalException` on iOS and tvOS, `JavascriptException` on
|
|
241
|
+
Android) is not reported a second time. An error in the render that first
|
|
242
|
+
mounts the provider comes before this capture is installed. Errors a boundary
|
|
243
|
+
catches are not reported automatically; use the callback below.
|
|
244
|
+
|
|
176
245
|
### React error boundary callback
|
|
177
246
|
|
|
178
247
|
An existing React class boundary can opt into handled capture. Render the
|
|
@@ -194,7 +263,7 @@ class Boundary extends React.Component<React.PropsWithChildren, { failed: boolea
|
|
|
194
263
|
|
|
195
264
|
export default function App() {
|
|
196
265
|
return (
|
|
197
|
-
<EverframeProvider config={{
|
|
266
|
+
<EverframeProvider config={{ sdkKey: '…' }}>
|
|
198
267
|
<Boundary>
|
|
199
268
|
<RootNavigator />
|
|
200
269
|
</Boundary>
|
|
@@ -233,17 +302,14 @@ const { attempted, accepted, duplicateSuppressed } = status.counters.handled;
|
|
|
233
302
|
|
|
234
303
|
Snapshots are detached, contain no captured content, reset on remount, and perform
|
|
235
304
|
no native query or upload. Paths are `handled` (including the boundary callback),
|
|
236
|
-
`errorUtils
|
|
305
|
+
`errorUtils` (including errors no boundary catches), and `rejection` (submission only). Each active attempt has one outcome:
|
|
237
306
|
`accepted`, `duplicateSuppressed`, `allowanceSuppressed`, `bridgeUnavailable`,
|
|
238
|
-
`nativeRefused`, `captureFailed`, `reentrantSuppressed`, `inactiveAborted
|
|
239
|
-
|
|
307
|
+
`nativeRefused`, `captureFailed`, `reentrantSuppressed`, or `inactiveAborted`.
|
|
308
|
+
Counters saturate at 2,147,483,647. Existing limits are ten
|
|
240
309
|
handled and ten automatic distinct keys; fatal capture bypasses them.
|
|
241
310
|
|
|
242
|
-
`accepted` requires exactly true from
|
|
243
|
-
not mean upload success, server processing or outbox removal.
|
|
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
|
|
311
|
+
`accepted` requires exactly true from the native capture method. It does
|
|
312
|
+
not mean upload success, server processing or outbox removal. This snapshot excludes native queue and
|
|
247
313
|
delivery observations; read those with `getReportDeliveryStatus()`. Status is
|
|
248
314
|
neutral `not-mounted` without a provider, `disabled` under the crash-reporting
|
|
249
315
|
veto, and `unsupported` on the browser root entry.
|
|
@@ -293,8 +359,7 @@ bridges. iOS marks `jvm-uncaught` unsupported and implies no automatic iOS nativ
|
|
|
293
359
|
crash collector.
|
|
294
360
|
|
|
295
361
|
Without a mounted provider the result is `not-mounted/no-mount`; the browser
|
|
296
|
-
export is `unsupported/platform`.
|
|
297
|
-
`unsupported/native-method-missing`. A throwing native call returns
|
|
362
|
+
export is `unsupported/platform`. A missing or throwing native call returns
|
|
298
363
|
`unavailable/native-call-failed`, and malformed native responses return
|
|
299
364
|
`unavailable/invalid-native-snapshot`. These fallbacks have an unobserved queue
|
|
300
365
|
and `unknown` platform policies. This API does not provide a per-report delivery
|
|
@@ -303,7 +368,7 @@ guarantee or trigger a retry.
|
|
|
303
368
|
## Network body capture (client veto)
|
|
304
369
|
|
|
305
370
|
```tsx
|
|
306
|
-
<EverframeProvider config={{
|
|
371
|
+
<EverframeProvider config={{ sdkKey: '…', networkBodies: { disabled: true } }}>
|
|
307
372
|
```
|
|
308
373
|
|
|
309
374
|
`networkBodies.disabled: true` is a **client veto** — it can only turn body
|
|
@@ -331,6 +396,34 @@ capturing network traffic in the first place:
|
|
|
331
396
|
Do not enable this option expecting RN's own network calls to show up in
|
|
332
397
|
reports — that capability doesn't exist yet.
|
|
333
398
|
|
|
399
|
+
## Crash-only sessions
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
<EverframeProvider config={{ sdkKey: '…', reporter: { disabled: true } }}>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`reporter.disabled: true` turns the bug reporter off for the session and
|
|
406
|
+
keeps crash and error reporting, for example in production builds. It is a
|
|
407
|
+
**client veto**: the dashboard can't turn the reporter back on for this
|
|
408
|
+
session.
|
|
409
|
+
|
|
410
|
+
In a crash-only session:
|
|
411
|
+
|
|
412
|
+
- Shake-to-report is off, and `open()` rejects with
|
|
413
|
+
`EverframeReporterDisabledError`.
|
|
414
|
+
- Session replay uses the app's crash-only sampling rate, which is 0 by
|
|
415
|
+
default. An error carries a replay only while the session records.
|
|
416
|
+
- Network bodies aren't captured.
|
|
417
|
+
- Crash and error capture, breadcrumbs, vitals, release health and the
|
|
418
|
+
companion work as usual.
|
|
419
|
+
|
|
420
|
+
A companion bond (a phone scanning the QR code, or a dashboard member
|
|
421
|
+
attaching) upgrades the session. Replay and network bodies then follow the
|
|
422
|
+
app's normal settings for the rest of the session, and recording starts at
|
|
423
|
+
the bond. The reporter stays off: reports from an upgraded session come
|
|
424
|
+
through the companion. See
|
|
425
|
+
[crash-only sessions](../../docs/session-replay.md#crash-only-sessions).
|
|
426
|
+
|
|
334
427
|
## Navigation breadcrumbs
|
|
335
428
|
|
|
336
429
|
Mark screens with one line; the native SDK derives `from → to` from a global
|
|
@@ -386,7 +479,7 @@ import { createNavigationContainerRef } from '@react-navigation/native';
|
|
|
386
479
|
const navigationRef = createNavigationContainerRef();
|
|
387
480
|
const txNav = reactNavigationIntegration({ navigationRef });
|
|
388
481
|
|
|
389
|
-
<EverframeProvider config={{
|
|
482
|
+
<EverframeProvider config={{ sdkKey, integrations: [consoleIntegration(), txNav] }}>
|
|
390
483
|
<NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
|
|
391
484
|
...
|
|
392
485
|
```
|
|
@@ -415,7 +508,7 @@ inside a native view and never reaches JS.
|
|
|
415
508
|
### Config
|
|
416
509
|
|
|
417
510
|
```tsx
|
|
418
|
-
<EverframeProvider config={{
|
|
511
|
+
<EverframeProvider config={{ sdkKey, vitals: { enabled: true, sampleRate: 0.5, captureSourceQuery: false } }}>
|
|
419
512
|
```
|
|
420
513
|
|
|
421
514
|
- `enabled` — absent follows the dashboard toggle; `false` opts out locally;
|
|
@@ -426,7 +519,9 @@ inside a native view and never reaches JS.
|
|
|
426
519
|
|
|
427
520
|
**Reconfiguring.** A Provider remount with the *same* config does not restart
|
|
428
521
|
the SDK: both native sides compare the incoming config against the *installed*
|
|
429
|
-
one and skip the start entirely. A **changed** config does restart it
|
|
522
|
+
one and skip the start entirely. A **changed** config does restart it (on iOS,
|
|
523
|
+
except a change confined to `releaseHealth` or the loaded bundle; see
|
|
524
|
+
[Release health](#release-health)) — and a
|
|
430
525
|
start supersedes the running SDK, detaching every player integration it had
|
|
431
526
|
announced. **Limitation:** players tracked before that restart stay untracked
|
|
432
527
|
until their screens remount; nothing re-registers them automatically. Two React
|
|
@@ -598,7 +693,7 @@ on Android), so React Native's focus engine never sees it — the native VC
|
|
|
598
693
|
manages focus on its own UIWindow / Activity.
|
|
599
694
|
|
|
600
695
|
```tsx
|
|
601
|
-
<EverframeProvider config={{
|
|
696
|
+
<EverframeProvider config={{ sdkKey }}>
|
|
602
697
|
<NavigationContainer>{/* focus engine root */}</NavigationContainer>
|
|
603
698
|
</EverframeProvider>
|
|
604
699
|
```
|
|
@@ -727,28 +822,12 @@ recipes above.
|
|
|
727
822
|
## Android reporter initialization
|
|
728
823
|
|
|
729
824
|
The Android reporter can be opened after the host activity has resumed, including
|
|
730
|
-
apps that disable AndroidX Startup.
|
|
731
|
-
`dev.everframe:reporter-ui` artifacts: the bridge supplies the current resumed
|
|
825
|
+
apps that disable AndroidX Startup. The bridge supplies the current resumed
|
|
732
826
|
activity, and installation and presentation run on the main thread. Repeated
|
|
733
|
-
configuration
|
|
734
|
-
remains synchronous without waiting for reporter UI initialization.
|
|
735
|
-
|
|
736
|
-
|
|
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.
|
|
827
|
+
configuration does not register another lifecycle observer. Capture configuration
|
|
828
|
+
remains synchronous without waiting for reporter UI initialization. A failure
|
|
829
|
+
while opening is reported to JavaScript. Background or unavailable activities
|
|
830
|
+
return a cancelled result with reason `no_active_activity`.
|
|
752
831
|
|
|
753
832
|
Activity destruction while a reporter is open resolves the pending call with
|
|
754
833
|
`{ status: "cancelled", reason: "activity_destroyed" }`, releases that report's
|
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.1,1.3.0)"
|
|
156
157
|
if (nativeVersionOverride != null) {
|
|
157
158
|
logger.lifecycle(
|
|
158
159
|
"[everframe-rn] native AAR pinned to $nativeVersionOverride via everframeNativeVersion " +
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// signalCompanionReportRequestReady(id) → void (Plan 06.2-13)
|
|
23
23
|
// trackPlayer/detachPlayer/recordPlayerEvent/updatePlayerStats/
|
|
24
24
|
// trackVitals → void (spec 2026-09-06)
|
|
25
|
-
//
|
|
25
|
+
// configureSync(opts.vitals*) → boolean (spec 2026-09-06)
|
|
26
26
|
// addListener(name)/removeListeners(n) → void (RN event-emitter no-ops)
|
|
27
27
|
//
|
|
28
28
|
// The native TXReporterPresenter owns the entire report UX (capture, annotate,
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
// and the sensitive-rect bridge. Native mobile shake stays below this bridge.
|
|
31
31
|
//
|
|
32
32
|
// THREADING:
|
|
33
|
-
// • configureSync runs on the JS thread (blocking synchronous)
|
|
34
|
-
// void configure retains React Native's queued native dispatch.
|
|
33
|
+
// • configureSync runs on the JS thread (blocking synchronous).
|
|
35
34
|
// • openReporter dispatches presenter.openReporter onto the activity from
|
|
36
35
|
// MainScope (presenter manages its own UI thread hops internally).
|
|
37
36
|
// • registerSensitiveRect runs on the UI thread (UIManager view resolution).
|
|
@@ -76,6 +75,8 @@ import dev.everframe.companion.CompanionSubmissionComposer
|
|
|
76
75
|
import dev.everframe.companion.PreviewCapture
|
|
77
76
|
import dev.everframe.companion.RelayWSClient
|
|
78
77
|
import dev.everframe.companion.parseCompanionBadgePosition
|
|
78
|
+
import dev.everframe.config.ReleaseHealthConfig
|
|
79
|
+
import dev.everframe.config.ReleaseHealthBundleStatus
|
|
79
80
|
import dev.everframe.config.CaptureConfig
|
|
80
81
|
import dev.everframe.config.Environment
|
|
81
82
|
import dev.everframe.config.ReportResult
|
|
@@ -146,6 +147,8 @@ class EverframeModule(
|
|
|
146
147
|
internal const val REJECT_CAPTURE_DISABLED: String = "capture_disabled"
|
|
147
148
|
internal const val REJECT_NO_ACTIVITY: String = "no_activity"
|
|
148
149
|
internal const val REJECT_OPEN_REPORTER_FAILED: String = "open_reporter_failed"
|
|
150
|
+
/** Crash-only sessions (spec 2026-10-12). The JS facade maps this exact code to `EverframeReporterDisabledError`. */
|
|
151
|
+
internal const val REJECT_REPORTER_DISABLED: String = "EVERFRAME_REPORTER_DISABLED"
|
|
149
152
|
|
|
150
153
|
// Plan 06.2-11 — JS-side event names. Keep in lock-step with
|
|
151
154
|
// `packages/sdk-react-native/src/companion.ts` and the iOS
|
|
@@ -173,13 +176,10 @@ class EverframeModule(
|
|
|
173
176
|
// below for the full contract.
|
|
174
177
|
internal const val COMPANION_ATTACH_CHALLENGE_EVENT: String =
|
|
175
178
|
"everframe.companion.attachChallenge"
|
|
176
|
-
// Fired when a `report.request` arrives from the paired phone.
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// `signalCompanionReportRequestReady` acknowledgement survive as the
|
|
181
|
-
// companion's JS-ready handshake — the bounded wait is part of the
|
|
182
|
-
// pairing flow, not of tap-to-identify.
|
|
179
|
+
// Fired when a `report.request` arrives from the paired phone. The
|
|
180
|
+
// event and its `signalCompanionReportRequestReady` acknowledgement
|
|
181
|
+
// are the companion's JS-ready handshake — the bounded wait is part
|
|
182
|
+
// of the pairing flow.
|
|
183
183
|
internal const val COMPANION_REPORT_REQUESTED_EVENT: String = "everframe.companion.reportRequested"
|
|
184
184
|
|
|
185
185
|
// Spec 2026-09-17 setExtra-resolver — NOT a companion event (no
|
|
@@ -207,20 +207,48 @@ class EverframeModule(
|
|
|
207
207
|
|
|
208
208
|
// ---------------- configure ----------------
|
|
209
209
|
|
|
210
|
-
@ReactMethod
|
|
211
|
-
override fun configure(opts: ReadableMap) {
|
|
212
|
-
configureInternal(opts)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
210
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
216
211
|
override fun configureSync(opts: ReadableMap): Boolean = configureInternal(opts)
|
|
217
212
|
|
|
213
|
+
private fun parseReleaseHealth(opts: ReadableMap): ReleaseHealthConfig? {
|
|
214
|
+
// Invalid bridge input revokes earlier monitoring instead of retaining an old subject.
|
|
215
|
+
// The SDK reads the app version itself; the bridge carries the loaded bundle, the user ID and
|
|
216
|
+
// whether debug builds count. Release health is on by default: a missing or null flag is on, and
|
|
217
|
+
// only an explicit false is off. It still needs a valid loaded bundle, so no id means off.
|
|
218
|
+
return runCatching {
|
|
219
|
+
if (opts.takeIfHasBoolean("releaseHealthEnabled") == false) return null
|
|
220
|
+
val loaded = opts.takeIfHasString("releaseHealthLoadedBuildId") ?: return null
|
|
221
|
+
val user = opts.takeIfHasString("releaseHealthUserId")
|
|
222
|
+
if (!validHealthText(loaded, 200) || (user != null && !validHealthText(user, 128))) return null
|
|
223
|
+
val includeDebugBuilds = opts.takeIfHasBoolean("releaseHealthIncludeDebugBuilds") ?: false
|
|
224
|
+
ReleaseHealthConfig(enabled = true, userId = user, loadedBuildId = loaded,
|
|
225
|
+
loadedBundleStatus = ReleaseHealthBundleStatus.KNOWN, includeDebugBuilds = includeDebugBuilds)
|
|
226
|
+
}.getOrNull()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private fun validHealthText(value: String, maximum: Int): Boolean {
|
|
230
|
+
// Match the wire's UTF-16 limit and ECMAScript blank definition, preserving ID bytes.
|
|
231
|
+
fun blank(c: Char) = c in '\u0009'..'\u000d' || c == ' ' || c == '\u00a0' ||
|
|
232
|
+
c == '\u1680' || c in '\u2000'..'\u200a' || c == '\u2028' || c == '\u2029' ||
|
|
233
|
+
c == '\u202f' || c == '\u205f' || c == '\u3000' || c == '\ufeff'
|
|
234
|
+
if (value.length > maximum || value.all(::blank)) return false
|
|
235
|
+
var index = 0
|
|
236
|
+
while (index < value.length) {
|
|
237
|
+
val c = value[index++]
|
|
238
|
+
if (c.code < 32 || Character.isLowSurrogate(c)) return false
|
|
239
|
+
if (Character.isHighSurrogate(c)) {
|
|
240
|
+
if (index >= value.length || !Character.isLowSurrogate(value[index++])) return false
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return true
|
|
244
|
+
}
|
|
245
|
+
|
|
218
246
|
private fun configureInternal(opts: ReadableMap): Boolean {
|
|
219
247
|
var completed = false
|
|
220
248
|
txGuardSurface("configure") {
|
|
221
|
-
val sdkKey = opts.takeIfHasString("
|
|
249
|
+
val sdkKey = opts.takeIfHasString("sdkKey")
|
|
222
250
|
?: throw JSApplicationIllegalArgumentException(
|
|
223
|
-
"configure: required field '
|
|
251
|
+
"configure: required field 'sdkKey' missing",
|
|
224
252
|
)
|
|
225
253
|
val sdkVersion = opts.takeIfHasString("sdkVersion") ?: Everframe.SDK_VERSION
|
|
226
254
|
|
|
@@ -233,6 +261,13 @@ class EverframeModule(
|
|
|
233
261
|
// Polarity flips exactly once, here: JS sends the veto
|
|
234
262
|
// (`installIdentifierDisabled`), native stores the permission.
|
|
235
263
|
val installIdentifierDisabled = opts.takeIfHasBoolean("installIdentifierDisabled") ?: false
|
|
264
|
+
// Polarity flips exactly once, here: JS sends the veto
|
|
265
|
+
// (`crashReportingDisabled`), native stores the permission.
|
|
266
|
+
val crashReportingDisabled = opts.takeIfHasBoolean("crashReportingDisabled") ?: false
|
|
267
|
+
// Polarity flips exactly once, here: JS sends the crash-only switch
|
|
268
|
+
// (`reporterDisabled`, flattened from `reporter: { disabled }`),
|
|
269
|
+
// native stores the permission.
|
|
270
|
+
val reporterDisabled = opts.takeIfHasBoolean("reporterDisabled") ?: false
|
|
236
271
|
// Attach-PIN UI mode (spec 2026-08-19) — a flat optional STRING
|
|
237
272
|
// field (codegen cannot express a string-literal union), parsed
|
|
238
273
|
// here and stashed for `startCompanion()` to read (the flag is a
|
|
@@ -245,12 +280,20 @@ class EverframeModule(
|
|
|
245
280
|
val attachPinUi = parseAttachPinUi(opts.takeIfHasString("attachPinUi"))
|
|
246
281
|
|
|
247
282
|
val config = EverframeConfig(
|
|
283
|
+
// The JS config carries no App ID, and the native SDK requires a
|
|
284
|
+
// non-blank one. It only keys this app's low-memory-kill issue, so
|
|
285
|
+
// the SDK key stands in, as it always has; changing it would split
|
|
286
|
+
// existing React Native low-memory issues.
|
|
248
287
|
appId = sdkKey,
|
|
249
288
|
sdkKey = sdkKey,
|
|
250
289
|
environment = Environment.production,
|
|
251
290
|
release = sdkVersion,
|
|
252
|
-
capture = CaptureConfig(networkBodies = !networkBodiesDisabled),
|
|
291
|
+
capture = CaptureConfig(networkBodies = !networkBodiesDisabled, crash = !crashReportingDisabled),
|
|
253
292
|
installIdentifierEnabled = !installIdentifierDisabled,
|
|
293
|
+
releaseHealth = parseReleaseHealth(opts),
|
|
294
|
+
// The JS facade always sends React Native's `__DEV__` (or the host's override). A missing
|
|
295
|
+
// or null key stays null so the native SDK decides for itself, as for any other caller.
|
|
296
|
+
debugBuild = opts.takeIfHasBoolean("debugBuild"),
|
|
254
297
|
// Explicit companion device-identity override (naming spec
|
|
255
298
|
// 2026-08-24) — read straight off the flat ConfigOpts field,
|
|
256
299
|
// same shape as attachPinUi above. Absent/blank falls
|
|
@@ -268,6 +311,7 @@ class EverframeModule(
|
|
|
268
311
|
// `startCompanion()` time, mirroring `attachPinUi`.
|
|
269
312
|
companionBadgeEnabled = opts.takeIfHasBoolean("companionBadgeEnabled") ?: true,
|
|
270
313
|
shakeToReportEnabled = opts.takeIfHasBoolean("shakeToReportEnabled") ?: true,
|
|
314
|
+
reporterEnabled = !reporterDisabled,
|
|
271
315
|
companionBadgePosition = opts.takeIfHasString("companionBadgePosition"),
|
|
272
316
|
// Inline reporter theme (reporter branding spec 2026-08-25,
|
|
273
317
|
// RN slice) — 8 flat optional STRING fields on ConfigOpts
|
|
@@ -377,6 +421,9 @@ class EverframeModule(
|
|
|
377
421
|
* { status: "submitted", reportId: "<uuid>" }
|
|
378
422
|
* { status: "queued", reportId: "<uuid>" }
|
|
379
423
|
* { status: "cancelled" }
|
|
424
|
+
*
|
|
425
|
+
* Rejects with [REJECT_REPORTER_DISABLED] in a crash-only session
|
|
426
|
+
* (`reporterDisabled`), including after a companion bond upgraded it.
|
|
380
427
|
*/
|
|
381
428
|
@ReactMethod
|
|
382
429
|
override fun openReporter(promise: Promise) {
|
|
@@ -384,6 +431,12 @@ class EverframeModule(
|
|
|
384
431
|
promise.reject(REJECT_CAPTURE_DISABLED, "Everframe not started (captureGate=false). Call configure() first.")
|
|
385
432
|
return
|
|
386
433
|
}
|
|
434
|
+
// Crash-only session: rejected here because ReporterBridge reduces every core failure to one
|
|
435
|
+
// generic message, which would hide `ReporterDisabledException` from JS.
|
|
436
|
+
if (Everframe.currentConfig?.reporterEnabled == false) {
|
|
437
|
+
promise.reject(REJECT_REPORTER_DISABLED, "The reporter is turned off for this session (reporter.disabled).")
|
|
438
|
+
return
|
|
439
|
+
}
|
|
387
440
|
MainScope().launch {
|
|
388
441
|
try {
|
|
389
442
|
val result: ReportResult = reporterBridge.open(reactContext)
|
|
@@ -454,7 +507,7 @@ class EverframeModule(
|
|
|
454
507
|
* `Everframe.addBreadcrumb` (Task 9) — NO validation/coercion here. That
|
|
455
508
|
* singleton already owns kind/level/data coercion AND gating (no-op
|
|
456
509
|
* pre-start / while killed via its own `txGuardVoid` + `captureGate`
|
|
457
|
-
* check), exactly like `setExtra
|
|
510
|
+
* check), exactly like `setExtra` above never re-checks
|
|
458
511
|
* `captureGate` themselves. `data` crosses as a `ReadableMap?` — the
|
|
459
512
|
* codegen-typed freeform-object boundary — converted with RN's own
|
|
460
513
|
* `toHashMap()` (no re-shaping) before handing off.
|
|
@@ -591,7 +644,7 @@ class EverframeModule(
|
|
|
591
644
|
*
|
|
592
645
|
* The primary fix is the JS projection in `src/user-projection.ts`, applied
|
|
593
646
|
* before the value ever crosses the bridge. This is the second layer, for a
|
|
594
|
-
* hand-wired native caller
|
|
647
|
+
* hand-wired native caller that reaches
|
|
595
648
|
* this method directly: bad fields degrade to `null` — a partial user, or
|
|
596
649
|
* anonymous — and `Everframe.setUser` is ALWAYS reached, so the call always
|
|
597
650
|
* replaces what was installed before.
|
|
@@ -641,11 +694,11 @@ class EverframeModule(
|
|
|
641
694
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
642
695
|
override fun getReportDeliveryStatusJson(): String = Everframe.getReportDeliveryStatus().toJson()
|
|
643
696
|
|
|
644
|
-
/** Automatic ErrorUtils capture; true acknowledges completed sidecar storage. */
|
|
697
|
+
/** Automatic ErrorUtils and unhandled-rejection capture; true acknowledges completed sidecar storage. A non-fatal error may carry a replay. */
|
|
645
698
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
646
699
|
override fun reportCrash(crashJson: String): Boolean = captureCrashJson(crashJson, handled = false)
|
|
647
700
|
|
|
648
|
-
/** Explicit capture always enforces handled=true, fatal=false, captureException. */
|
|
701
|
+
/** Explicit capture always enforces handled=true, fatal=false, captureException, and may carry a replay. */
|
|
649
702
|
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
650
703
|
override fun captureHandledException(crashJson: String): Boolean = captureCrashJson(crashJson, handled = true)
|
|
651
704
|
|
|
@@ -672,12 +725,17 @@ class EverframeModule(
|
|
|
672
725
|
val details = if (obj.has("details")) obj.get("details") else null
|
|
673
726
|
val causeChain = if (obj.has("causeChain")) obj.get("causeChain") else null
|
|
674
727
|
val fatal = !handled && obj.optBoolean("fatal", false)
|
|
675
|
-
|
|
676
|
-
|
|
728
|
+
// Only errors the app survives ask for a replay. Eligibility and export remain in the core;
|
|
729
|
+
// fatal errors keep the entry point that never carries a replay.
|
|
730
|
+
val accepted = when {
|
|
731
|
+
handled -> dev.everframe.crash.CrashReporter.captureHandledFactsWithReplay(
|
|
677
732
|
exceptionType, message, frames, occurredAt, jsBundle, details, causeChain, "everframe-android",
|
|
678
733
|
)
|
|
679
|
-
|
|
680
|
-
|
|
734
|
+
fatal -> dev.everframe.crash.CrashReporter.captureFactsAcceptedWithCauses(
|
|
735
|
+
exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
|
|
736
|
+
fatal, occurredAt, jsBundle, details, causeChain,
|
|
737
|
+
)
|
|
738
|
+
else -> dev.everframe.crash.CrashReporter.captureFactsAcceptedWithReplay(
|
|
681
739
|
exceptionType, message, frames, obj.optString("mechanism", "errorutils"),
|
|
682
740
|
fatal, occurredAt, jsBundle, details, causeChain,
|
|
683
741
|
)
|
|
@@ -1051,14 +1109,6 @@ class EverframeModule(
|
|
|
1051
1109
|
val capture: ScreenshotCapture.CaptureResult,
|
|
1052
1110
|
val reportCapture: dev.everframe.capture.video.FrozenReportCapture,
|
|
1053
1111
|
val hostExtra: String?,
|
|
1054
|
-
/**
|
|
1055
|
-
* Drained-and-discarded host React tree (spec 2026-08-29). Nothing
|
|
1056
|
-
* reads it any more, and RN can no longer attach one — the
|
|
1057
|
-
* TurboModule spec method is gone. Kept only because the native core
|
|
1058
|
-
* `Everframe.attachReactTree` remains callable by pure-native hosts,
|
|
1059
|
-
* so the consume-once drain must still clear the slot.
|
|
1060
|
-
*/
|
|
1061
|
-
val hostReactTreeJson: String?,
|
|
1062
1112
|
)
|
|
1063
1113
|
|
|
1064
1114
|
private val companionCaptureStash = ConcurrentHashMap<String, CompanionCapture>()
|
|
@@ -1115,7 +1165,7 @@ class EverframeModule(
|
|
|
1115
1165
|
// same reason the screenshot capture above is —
|
|
1116
1166
|
// this whole provider runs on the OkHttp WS
|
|
1117
1167
|
// thread, never the UI thread.
|
|
1118
|
-
val
|
|
1168
|
+
val hostExtraRaw =
|
|
1119
1169
|
runBlocking { Everframe.consumePendingAttachments() }
|
|
1120
1170
|
val hostExtra = hostExtraRaw?.takeIf { it.isNotEmpty() }
|
|
1121
1171
|
|
|
@@ -1127,7 +1177,6 @@ class EverframeModule(
|
|
|
1127
1177
|
capture = capture,
|
|
1128
1178
|
reportCapture = reportCapture,
|
|
1129
1179
|
hostExtra = hostExtra,
|
|
1130
|
-
hostReactTreeJson = hostReactTreeJson?.takeIf { it.isNotEmpty() },
|
|
1131
1180
|
)
|
|
1132
1181
|
}
|
|
1133
1182
|
if (!published) {
|
|
@@ -1439,9 +1488,7 @@ class EverframeModule(
|
|
|
1439
1488
|
/**
|
|
1440
1489
|
* Called by the JS companion module from the `everframe.companion
|
|
1441
1490
|
* .reportRequested` event handler once it is ready for capture to
|
|
1442
|
-
* proceed.
|
|
1443
|
-
* `attachReactTree` first; both are gone — spec 2026-08-29.) Releases
|
|
1444
|
-
* the `awaitJsReactTreeAttach` waiter for this correlation id so the
|
|
1491
|
+
* proceed. Releases the `awaitJsReactTreeAttach` waiter for this correlation id so the
|
|
1445
1492
|
* capture provider can proceed. Idempotent — late signals after the
|
|
1446
1493
|
* timeout fired are harmless.
|
|
1447
1494
|
*/
|
|
@@ -9,7 +9,6 @@ import android.os.Looper
|
|
|
9
9
|
import android.util.Log
|
|
10
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
11
|
import com.facebook.react.common.LifecycleState
|
|
12
|
-
import dev.everframe.Everframe
|
|
13
12
|
import dev.everframe.config.ReportResult
|
|
14
13
|
import java.lang.reflect.InvocationTargetException
|
|
15
14
|
import java.lang.reflect.Method
|
|
@@ -19,7 +18,7 @@ import kotlinx.coroutines.CancellationException
|
|
|
19
18
|
import kotlinx.coroutines.Dispatchers
|
|
20
19
|
import kotlinx.coroutines.withContext
|
|
21
20
|
|
|
22
|
-
/**
|
|
21
|
+
/** Resolves the reporter installer reflectively, so tests can substitute it. */
|
|
23
22
|
internal class ReporterBridge(
|
|
24
23
|
private val resolveInstaller: () -> Class<*> = { Class.forName("dev.everframe.ui.ReporterResolverInstaller") },
|
|
25
24
|
) {
|
|
@@ -46,17 +45,9 @@ internal class ReporterBridge(
|
|
|
46
45
|
try {
|
|
47
46
|
val type = installerType()
|
|
48
47
|
val instance = type.getDeclaredConstructor().newInstance()
|
|
49
|
-
val method =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (method == null) {
|
|
53
|
-
// Absence is the only legacy fallback. A present method's failure is final.
|
|
54
|
-
invoke(type.getMethod("create", Context::class.java), instance, context.applicationContext)
|
|
55
|
-
Everframe.report.open()
|
|
56
|
-
} else {
|
|
57
|
-
suspendCoroutineUninterceptedOrReturn<ReportResult> { continuation ->
|
|
58
|
-
invoke(method, instance, context.applicationContext, activity, continuation)
|
|
59
|
-
}
|
|
48
|
+
val method = type.getMethod("openForActivity", Context::class.java, Activity::class.java, Continuation::class.java)
|
|
49
|
+
suspendCoroutineUninterceptedOrReturn<ReportResult> { continuation ->
|
|
50
|
+
invoke(method, instance, context.applicationContext, activity, continuation)
|
|
60
51
|
}
|
|
61
52
|
} catch (failure: Throwable) {
|
|
62
53
|
when (failure) {
|