npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@everframe/react-native

v1.2.1

Published

React Native SDK for Everframe — AI-ready in-app bug reporting, bridging the native iOS and Android reporter to your RN app. Supports phone, Apple TV, and Android TV.

Readme

@everframe/react-native

Everframe React Native bridge — exposes the native iOS and Android reporter modal to RN host apps via a TurboModule + a thin React provider/hook API.

Phone, Apple TV, and Android TV are all supported when the host app is built against react-native-tvos (see TV Host Integration).


See the crash-reporting support matrix for exact tested paths, unsupported combinations and remaining qualification.

Installation

pnpm add @everframe/react-native

Peer dependencies: react ≥ 19, react-native (or react-native-tvos for TV targets) ≥ 0.85.

Then install pods (iOS) / sync Gradle (Android) as usual:

cd ios && pod install

Source maps and symbols

Release builds can upload their Hermes source maps, dSYMs, R8 mappings and native libraries, so crashes show your code. Wrap Metro with withEverframe from @everframe/metro, add @everframe/cli, and set up the native projects once. In Expo, apply withEverframe from @everframe/expo with your App ID in app.config.js. In a bare app, run:

npx @everframe/cli setup react-native --app-id "$EVERFRAME_APP_ID"

Either way, Android gets apply plugin: "dev.everframe" and everframe { appId = "…" } plus one classpath line, and Xcode gets a Run Script phase that runs everframe xcode build-phase. The upload logic lives in the Gradle plugin and the CLI. Set EVERFRAME_API_TOKEN in release builds; without it the build warns and continues. See the CLI guide.

React Native Web

@everframe/react-native has a browser entry and an explicit @everframe/react-native/web entry. Both re-export the existing @everframe/react package, which is installed as a dependency. Your app imports from @everframe/react-native on every platform:

pnpm add @everframe/react-native

React Native Web hosts also need React DOM, as usual. The browser entry uses the React web reporter, DOM screenshot, and rrweb replay. It does not load the native TurboModule. EverframeSensitive is an alias of the React SDK's Sensitive component on web, so shared app code can keep the same import. useEverframeSensitiveRef also registers a browser element with the web mask registry. Native iOS and Android use their native capture. A web bundler that honors the browser export condition can keep the regular package import; use the /web subpath if its resolver does not select that condition.

Replays also attach to errors; see session replay.

Usage

Wrap your app with <EverframeProvider> at the highest practical level (above any navigator / focus engine root):

import { EverframeProvider, useEverframe } from '@everframe/react-native';

export default function App() {
  return (
    // `sdkKey` is the only required field. The ingest endpoint is a
    // compile-time constant in the SDK and is not configurable in v1.
    <EverframeProvider
      config={{
        sdkKey: 'evf_live_xxxxxxxxxxxxxxxx',
        // Recommended in RN/Expo development: shake also opens the dev menu.
        shakeToReport: { enabled: !__DEV__ },
      }}
    >
      <RootNavigator />
    </EverframeProvider>
  );
}

Trigger the reporter from any host UI:

function HelpButton() {
  const { open } = useEverframe();
  return <Button title="Report a bug" onPress={() => open()} />;
}

For non-component contexts, use the top-level open re-export (throws EverframeNotMountedError if the provider is not yet mounted):

import { open } from '@everframe/react-native';
await open();

open() returns a ReporterResult with { status: 'submitted' | 'queued' | 'cancelled', ... }.

For handled JavaScript exceptions, call captureException(error) from the provider context and attach any non-sensitive context needed for diagnosis. The source API requires matching rebuilt native components; published 0.7.0 artifacts are not evidence of support.

Release health

Release health counts crash-free foreground sessions for each version of your app. It is on by default, and debug builds of the native app aren't counted. Turn it off in the EverframeProvider config:

releaseHealth: { enabled: false },

Sessions come from the Android and iOS SDKs and count toward the native app version, which an over-the-air update doesn't change. Release health needs the Hermes bundle identity that @everframe/metro adds at build time (or the jsBundle option); without it, release health stays off and the SDK logs an [everframe] warning (in development builds only, unless you set releaseHealth). Add userId: accountId to count crash-free users, and includeDebugBuilds: true to count debug builds too. The configuration is read when the Provider mounts, so remount it with a changed key to change enabled or userId. A configure with enabled: false turns release health off and erases its queued records on the device. Release health skips a debug build, which is __DEV__ unless you set debugBuild on the Provider. See release health.

Promise rejection observation

Automatic Hermes rejection observation is opt-in:

import { EverframeProvider, getPromiseRejectionStatus } from '@everframe/react-native';

<EverframeProvider config={{
  sdkKey: '…',
  crashReporting: { promiseRejections: { enabled: true } },
}}>
  <App />
</EverframeProvider>

// Read after mount. This reports observer coverage, not delivery confirmation.
const status = getPromiseRejectionStatus();

The verified runtime is Android/iOS [email protected] with Hermes release 250829098.0.10, Release/Static Hermes and bytecode 98. Qualification uses optimized apps on an Android API 35 arm64 emulator and an iOS 26.5 arm64 simulator with Debug/local native SDK transport, including expiry after the app process is suspended, source mapping and encrypted retry after relaunch. Physical devices, device sleep and production transport are not qualified by these checks. Matching the SDK's broader RN peer range does not establish rejection support. Other versions, JSC, browser execution, tvOS, unverified Promise replacements and incompatible hooks return unsupported. The adapter checks runtime identity and a fulfilled-only hook handshake; it does not generate a test rejection or replace the Promise constructor.

Rejections still unhandled after 2 seconds enter the existing nonfatal capture path. A handler attached within that interval cancels capture; later handling does not retract an accepted report. The notification uses a JavaScript timer. Android pauses JavaScript timers while the app's activity is paused (in the background, behind another activity or in picture-in-picture), although JavaScript can keep running. Rejections observed then are notified on later Promise activity or when the activity resumes. Explicit capture and automatic reporting share accepted error identity, and automatic reports retain their existing 10-distinct-key allowance per mount. Automatic reports are deduplicated by exception type and top stack frame. Non-Error reasons have no stack and are deduplicated by their reported value with digit runs ignored, so Request 1001 failed repeats Request 1000 failed and all object reasons share one report per mount. They spend at most 5 of the 10 keys, so values that differ in other ways, such as hex identifiers, still leave 5 keys for Error rejections and other automatic reports.

The observer retains at most 16 detached snapshots, each at most 64 KiB of serialized UTF-8 data. It drops new arrivals when full and oversize snapshots rather than retaining arbitrary error graphs. Work is counted as expired instead of reported when it is notified more than 30 seconds after the rejection. Age is measured on the platform's elapsed clock and bounded by wall-clock time, which keeps advancing while an Android device sleeps. Cleanup cannot run while JavaScript is suspended, so stale work expires when JavaScript resumes. Error facts and causes are snapshotted at rejection time. Rejection reasons that are not Error objects are reported as UnhandledValue without a stack or cause chain, even when they carry a cause. Strings, numbers, booleans, null and undefined keep their bounded value; objects, arrays, functions, symbols and bigints are reported only as a type label such as [object] rather than serialized. Native context and breadcrumbs are collected at notification time. A preserved external tracker may independently retain errors or generate its own reports.

getPromiseRejectionStatus() returns disabled, unsupported, observing, displaced, install-failed, or not-mounted, with a reason and bounded counters for pending, accepted, cancelled, dropped, expired, suppressed, refused, failed and discarded captures. A rejection handled within the 2-second interval counts as cancelled, even if it was dropped or failed to snapshot on arrival. Counters saturate at 2,147,483,647 and reset on each mount; they contain no captured messages or stacks. Replacing either hook stops this observer until a new mount and releases its pending work, counted as discarded. crashReporting.disabled: true overrides the opt-in, and unmount discards pending work. Configuration changes take effect on a new mount. The browser export reports Hermes observation as unsupported and preserves the browser SDK's own error handling.

Error capture and diagnostics

Native crashes and ANRs

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. On iOS it is the native crash recorder. crashReporting: { disabled: true } turns all of it off on both platforms.

Errors no boundary catches

Since React Native 0.78, React reports an error that no error boundary catches (thrown while rendering, in an effect or in a lifecycle method) through the root's onUncaughtError. That calls React Native's ExceptionsManager, never the ErrorUtils handler. Crash reporting hooks ExceptionsManager.handleException too, so such an error is reported as a fatal JavaScript crash (mechanism: "errorutils") with its Hermes stack, and React's component stack follows the message after This error is located at:, as in React Native's own report. React unmounts the whole tree, EverframeProvider included, before it reports the error; the hook stays until the end of that task, so the error is still captured. The native fatal React Native raises for the same error (RCTFatalException on iOS and tvOS, JavascriptException on Android) is not reported a second time. An error in the render that first mounts the provider comes before this capture is installed. Errors a boundary catches are not reported automatically; use the callback below.

React error boundary callback

An existing React class boundary can opt into handled capture. Render the boundary inside EverframeProvider:

import * as React from 'react';
import { EverframeProvider } from '@everframe/react-native';
import { captureReactError } from '@everframe/react-native/integrations/react';

class Boundary extends React.Component<React.PropsWithChildren, { failed: boolean }> {
  state = { failed: false };
  static getDerivedStateFromError() { return { failed: true }; }
  componentDidCatch(error: unknown, info: React.ErrorInfo) {
    captureReactError(error, info);
  }
  render() { return this.state.failed ? null : this.props.children; }
}

export default function App() {
  return (
    <EverframeProvider config={{ sdkKey: '…' }}>
      <Boundary>
        <RootNavigator />
      </Boundary>
    </EverframeProvider>
  );
}

A boundary that wraps EverframeProvider does not report through it: its fallback replaces the provider in the same commit, before componentDidCatch runs. Such a boundary reports only if its fallback renders its own EverframeProvider, which mounts before componentDidCatch runs. Errors that a boundary inside the provider catches during the provider's first commit (the initial render that also mounts the provider) are not captured, and they are not replayed. Disabled crash reporting and calls after unmount are also inert. In development builds, a call that finds no mounted provider logs a warning, at most once until a provider mounts or unmounts.

The application owns fallback and recovery UI. This callback reports caught component errors; it does not install global renderer hooks or capture arbitrary async/event-handler errors. Component-stack metadata is bounded and redacted by the existing capture pipeline, without replacing exception frames or cause data.

The original error shares accepted identity with manual and nonfatal automatic capture within a mount. Native refusal permits recapture; equal fingerprint keys can also suppress distinct objects. A later fatal escalation may report separately. The adapter subpath is unavailable to browser-conditioned consumers.

Local capture admission diagnostics

import { getErrorCaptureStatus } from '@everframe/react-native';
const status = getErrorCaptureStatus();
const { attempted, accepted, duplicateSuppressed } = status.counters.handled;

Snapshots are detached, contain no captured content, reset on remount, and perform no native query or upload. Paths are handled (including the boundary callback), errorUtils (including errors no boundary catches), and rejection (submission only). Each active attempt has one outcome: accepted, duplicateSuppressed, allowanceSuppressed, bridgeUnavailable, nativeRefused, captureFailed, reentrantSuppressed, or inactiveAborted. Counters saturate at 2,147,483,647. Existing limits are ten handled and ten automatic distinct keys; fatal capture bypasses them.

accepted requires exactly true from the native capture method. It does not mean upload success, server processing or outbox removal. This snapshot excludes native queue and delivery observations; read those with getReportDeliveryStatus(). Status is neutral not-mounted without a provider, disabled under the crash-reporting veto, and unsupported on the browser root entry.

Installed boundary qualification passes on Android API 35 arm64 emulator and iOS 26.5 arm64 simulator for this exact phone-host row: React 19.2.5, react-native-tvos 0.85.3-0 aliased as react-native, Expo 56.0.0-preview.7, Hermes 250829098.0.10 / bytecode 98. Other RN distributions, versions, TV, physical devices and production endpoints are not qualified by this matrix. Installed checks cover four genuine boundary capture cases, disabled/initial zero controls, mapped causes, and byte-identical encrypted retry after relaunch. The application/JS builds are optimized; native SDKs are Debug/local. Physical-device and production-endpoint qualification remains open.

Native report delivery diagnostics

getReportDeliveryStatus() synchronously copies cached native observations. It performs no file reads, uploads, drains, or hook installation. Use getErrorCaptureStatus() separately for JavaScript admission decisions.

import { getReportDeliveryStatus } from '@everframe/react-native';
const status = getReportDeliveryStatus();
console.log(status.status, status.queue.observation, status.queue.pendingCount);

The schema separates native capture acceptance, queue operations and settled transport attempts (live-submit and outbox-drain). server-accepted means an HTTP 2xx response; it does not establish processing, symbolication, dashboard visibility or queue removal. Removal has its own operation counter. Counters are operations, not unique reports, and saturate at 2,147,483,647.

The queue is the SDK's shared report outbox, including reporter submissions and entries from earlier configurations. Its count is last-observed, not a fresh measurement or a current-project/crash-only total. Missing counts mean unknown; partial means only readable entries were counted. Android rejects new entries at capacity and retains terminal HTTP responses during drain. iOS can evict older entries and attempts removal after terminal HTTP responses. Custom standalone outbox/submitter instances are not automatically included.

Snapshots contain only fixed codes and counters, never report content, IDs, URLs, credentials or exception strings. Observations are process-local and reset on native start/reconfiguration or kill. They are best-effort: contention and termination may lose observations; a busy getter reports snapshot-busy. bridge-handled and bridge-automatic cover fact entry paths shared by framework bridges. iOS marks jvm-uncaught unsupported and implies no automatic iOS native crash collector.

Without a mounted provider the result is not-mounted/no-mount; the browser export is unsupported/platform. A missing or throwing native call returns unavailable/native-call-failed, and malformed native responses return unavailable/invalid-native-snapshot. These fallbacks have an unobserved queue and unknown platform policies. This API does not provide a per-report delivery guarantee or trigger a retry.

Network body capture (client veto)

<EverframeProvider config={{ sdkKey: '…', networkBodies: { disabled: true } }}>

networkBodies.disabled: true is a client veto — it can only turn body capture off locally; it can never turn it on. The server's per-app captureBodies gate is still authoritative, and native network capture must still be wired up on each platform (see below) before any body is ever recorded.

This option does not, by itself, make React Native capture network bodies. RN does not currently attach Everframe's network capture to its own HTTP clients (fetch, Axios, etc.) — that wiring is tracked separately and is not part of this SDK yet. RN apps do still inherit the native SDK's own auto-capture running underneath them, so networkBodies controls that native capture — and even that requires the native side to actually be capturing network traffic in the first place:

  • Android: bodies are captured only for OkHttpClient instances the host app explicitly builds with addEverframeInterceptor() (see packages/sdk-android/android/README.md). Without that interceptor, no network capture — and therefore no bodies — happens regardless of this option.
  • iOS: equivalent native network capture wiring, if and when the host app uses it.

Do not enable this option expecting RN's own network calls to show up in reports — that capability doesn't exist yet.

Crash-only sessions

<EverframeProvider config={{ sdkKey: '…', reporter: { disabled: true } }}>

reporter.disabled: true turns the bug reporter off for the session and keeps crash and error reporting, for example in production builds. It is a client veto: the dashboard can't turn the reporter back on for this session.

In a crash-only session:

  • Shake-to-report is off, and open() rejects with EverframeReporterDisabledError.
  • Session replay uses the app's crash-only sampling rate, which is 0 by default. An error carries a replay only while the session records.
  • Network bodies aren't captured.
  • Crash and error capture, breadcrumbs, vitals, release health and the companion work as usual.

A companion bond (a phone scanning the QR code, or a dashboard member attaching) upgrades the session. Replay and network bodies then follow the app's normal settings for the rest of the session, and recording starts at the bond. The reporter stays off: reports from an upgraded session come through the companion. See crash-only sessions.

Navigation breadcrumbs

Mark screens with one line; the native SDK derives from → to from a global chain shared with native auto-capture. Works with any navigation approach.

react-navigation (screens stay mounted — pass focus):

import { useEverframeScreen } from '@everframe/react-native';
import { useIsFocused, useRoute } from '@react-navigation/native';

function DetailScreen() {
  useEverframeScreen(useRoute().name, { focused: useIsFocused() });
  ...
}

…or whole-app in one place:

<NavigationContainer ref={navRef}
  onStateChange={() => recordScreen(navRef.getCurrentRoute()?.name ?? '')}>

Wix react-native-navigation:

componentDidAppear() { recordScreen(this.props.screenName); }

Hand-rolled (conditional-render tabs, custom switchers):

function DeskTab() {
  useEverframeScreen('Desk');
  ...
}

Screen names should be route identifiers, never user content.

Opt-in JS integrations

Native capture (taps, screens, lifecycle, errors) is automatic. Two things live only in JS — console logs and navigator state — and are captured ONLY when you opt in:

import { consoleIntegration } from '@everframe/react-native/integrations/console';
import { reactNavigationIntegration } from '@everframe/react-native/integrations/react-navigation';
import { createNavigationContainerRef } from '@react-navigation/native';

const navigationRef = createNavigationContainerRef();
const txNav = reactNavigationIntegration({ navigationRef });

<EverframeProvider config={{ sdkKey, integrations: [consoleIntegration(), txNav] }}>
  <NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
    ...
  • consoleIntegration({ levels? }) — forwards console.log/info/warn/error (configurable) to the breadcrumb trail with real severity. Originals always run first.
  • reactNavigationIntegration({ navigationRef }) — records every route change via recordScreen; also covers expo-router. Pass txNav.onReady to the container so the initial route is recorded.

Using another navigator? Any stack is a ~5-line adapter over recordScreen — the useEverframeScreen recipes above cover Wix RNN and hand-rolled navigation, and a custom integration is just { name, setup() { ...subscribe...; return unsubscribe } }.

Session Vitals

Native capture again: every RN app automatically gets background CPU/memory sampling and a session id stamped on reports and crashes as soon as the host app's project has vitals enabled on the dashboard — no JS change required.

What JS adds is player tracking, because in RN the player instance lives inside a native view and never reaches JS.

Config

<EverframeProvider config={{ sdkKey, vitals: { enabled: true, sampleRate: 0.5, captureSourceQuery: false } }}>
  • enabled — absent follows the dashboard toggle; false opts out locally; true never forces vitals on if the dashboard has them off.
  • sampleRate — [0, 1], min()'d with the server-configured rate.
  • captureSourceQuery — keep the query string on source_change.src (default false: signed CDN URLs carry tokens in the query).

Reconfiguring. A Provider remount with the same config does not restart the SDK: both native sides compare the incoming config against the installed one and skip the start entirely. A changed config does restart it (on iOS, except a change confined to releaseHealth or the loaded bundle; see Release health) — and a start supersedes the running SDK, detaching every player integration it had announced. Limitation: players tracked before that restart stay untracked until their screens remount; nothing re-registers them automatically. Two React instances in one process are not coordinated either: if a second one starts the SDK under the first, the first instance's players stay untracked until its own screens remount.

react-native-video (v7)

import { useVideoPlayer, VideoView } from 'react-native-video';
import { useVideoPlayerVitals } from '@everframe/react-native/integrations/react-native-video';

function Player() {
  const player = useVideoPlayer(source);
  useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });
  return <VideoView player={player} />;
}

libraryVersion is a host-supplied option — react-native-video does not expose it at runtime.

To measure INITIAL startup, defer the load:

// Module scope: the latch belongs to the PLAYER, not to the component. A
// replacement instance (StrictMode's double-mount, a source change) is a
// different object and gets its own initialize; the same instance never
// gets a second one, which would restart playback under the user.
const initialised = new WeakSet<object>();

const player = useVideoPlayer({ uri: SOURCE, initializeOnCreation: false });
useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });

useEffect(() => {
  if (initialised.has(player)) return;
  initialised.add(player);
  void player.initialize();
}, [player]);

Why: on Android react-native-video v7 emits onLoadStart synchronously from inside the native player constructor — before any effect runs, so before the adapter has subscribed — and onLoadStart is what arms the adapter's startup clock, so the first source's startup latency is simply never seen. Deferring the load until after the adapter subscribed puts the first onLoadStart back where it can be observed. Without this, everything else still works; only the first startup measurement is missing (later source changes are fine).

Options: name, libraryVersion, and captureErrors (default false — see Limitations). onBandwidthUpdate is read per platform: on iOS its bitrate is the rendition's declared bitrate and drives bitrate_change plus the bitrate stat; on Android it is ExoPlayer's bandwidth estimate, reported as the bandwidthEstimate stat (with quality_change for the accompanying rendition size) and never as a bitrate.

onProgress is read per platform too. The bufferAheadMs stat is always the media time buffered ahead of the playhead: on Android bufferDuration is already that, on iOS it is the buffered range's absolute end position, so the adapter subtracts currentTime from it.

THEOplayer

import { attachTheoPlayerVitals } from '@everframe/react-native/integrations/theoplayer';

<THEOplayerView onPlayerReady={(player) => attachTheoPlayerVitals(player, { name: 'main' })} />

Both adapters are pure translators: neither library is imported at runtime or added as a dependency of this package.

Custom log lines

import { trackVitals } from '@everframe/react-native';

trackVitals('ad_break', { pod: 1 });

Any other player: trackPlayer

Libraries without an adapter above (or a player object outside a component tree) can drive the same pipeline directly:

import { trackPlayer } from '@everframe/react-native';

const handle = trackPlayer({ library: 'my-player', libraryVersion: '1.2.0', name: 'main' });
handle.emit('play');
handle.emit('buffer_start');
handle.emit('buffer_end', { durationMs: 800 });
handle.updateStats({ bufferAheadMs: 4200, bitrate: 2_500_000 });
handle.track('ad_break', { pod: 1 });
handle.detach();

useTrackPlayer(opts) is the hook form — tracks on mount, detaches on unmount, mints a fresh token on remount (Fast Refresh-safe).

Event vocabulary for emit: source_change, drm, startup, play, pause, buffer_start, buffer_end, seek, rate_change, bitrate_change, quality_change, dropped_frames, error, stats. player_attach and player_detach are reserved for the native lifecycle markers the registry emits itself — passing either to emit is silently dropped, not forwarded.

Limitations

  • JS-thread stalls pause player events. Startup timing, span derivation and stats all happen in JS, so a wedged JS thread stalls the player's vitals while it is wedged. Native CPU/memory sampling keeps running regardless — it does not go through JS.
  • Dropped frames are reported only where the underlying library exposes them; the vocabulary has the slot, adapters fill it when they can.
  • The ten-per-minute non-fatal error budget counts forwarding attempts, including ones native drops — e.g. before vitals start, or against a token whose registration was refused. PlayerHandle.emit is void (fire-and-forget across the bridge), so JS cannot know whether native admitted an entry; an error refused over there still spends a slot.
  • captureErrors changes react-native-video's own behaviour, so it is off by default. With it on, errors carry the library's code ('source/invalid-uri', …). But v7 only throws synchronously from play() / pause() / seekBy() / seekTo() / selectTextTrack() / getAvailableTextTracks() when NO onError listener is registered — so merely subscribing makes those calls stop throwing for the whole app, and a host relying on try { player.play() } catch would silently lose its errors. Off by default, fatal errors are still reported (via onStatusChange('error')), just without a code.
  • captureErrors keeps swallowing after detach. react-native-video's listener removal leaves an EMPTY onError listener set rather than no set at all, and the library's "throw synchronously when nobody is listening" check only asks whether the set exists. So once captureErrors: true has been attached even once, that player keeps suppressing the synchronous play() / seek*() throws for the rest of its lifetime — detaching the vitals adapter does not give them back. A library bug, not an adapter one; there is nothing to fix on our side, which is the second reason captureErrors is off by default.
  • rate_change was not observed from react-native-video v7 on Android in our Android emulator smoke — onPlaybackRateChange never arrived there. Note that the library does forward ExoPlayer's onPlaybackParametersChanged, so this is a "not seen in our smoke run" result, not a proven library gap; a host that changes playback speed explicitly may well see it. Nothing on the adapter side gates it either way.
  • react-native-video v7 does not support tvOS. As of 7.0.0-beta.11 there is no tvOS podspec/target, so it cannot be used on Apple TV at all. Use THEOplayer, or a custom trackPlayer/PlayerHandle adapter, on Apple TV hosts.
  • A native react-native-video plugin (attaching the real ExoPlayer/AVPlayer instance directly, for full fidelity including dropped frames) is a follow-up once this JS-forwarding seam has proven itself in the field. Every library ends on ExoPlayer/AVPlayer, but JS event forwarding is what works universally today.

TV Host Integration (Apple TV + Android TV)

@everframe/react-native supports Apple TV + Android TV when consumed from a host app built against react-native-tvos. This support is developed against RN-tvos 0.85.3-0, Expo SDK 56.0.0-preview.7, and React 19.2.5; the canonical example app below is kept building on that matrix.

Provider placement

<EverframeProvider> MUST sit ABOVE the TV focus engine's root in the component tree. The reporter is presented imperatively by the native side (via TXTVReporterViewController on iOS / :everframe-tv ReporterActivity on Android), so React Native's focus engine never sees it — the native VC manages focus on its own UIWindow / Activity.

<EverframeProvider config={{ sdkKey }}>
  <NavigationContainer>{/* focus engine root */}</NavigationContainer>
</EverframeProvider>

Triggers are the host app's concern

Everframe installs shake-to-report on Android and iOS phones/tablets through the native SDKs. It is enabled by default, requires no permission, safely no-ops on Android devices without an accelerometer, and never runs on Android TV or tvOS. The dashboard is authoritative: local enabled: true cannot override a dashboard disable.

In React Native and Expo development builds, use shakeToReport: { enabled: !__DEV__ } because the React Native development menu also uses shake. Production remains enabled by default.

All other gestures and keys are host-owned. The host app calls useEverframe().open() (or the top-level open()) from whatever trigger makes sense for the target form factor.

For TV, the canonical pattern uses TVEventHandler (exposed by react-native-tvos).

API shape ([email protected])

import { TVEventHandler } from 'react-native';

type HWEvent = {
  eventType:
    | 'menu' | 'playPause' | 'longPlayPause' | 'select'
    | 'up' | 'down' | 'left' | 'right'
    | 'longUp' | 'longDown' | 'longLeft' | 'longRight'
    | 'pan' | string;
  eventKeyAction?: -1 | 0 | 1 | number;   // 0 = down, 1 = up, -1 = unknown
  tag?: number;
  body?: { state: 'Began' | 'Changed' | 'Ended'; x: number; y: number; velocityX: number; velocityY: number };
};

const subscription = TVEventHandler.addListener((evt: HWEvent) => { /* ... */ });
subscription?.remove();

If you have seen the older new TVEventHandler(); handler.enable(cmp, cb); handler.disable() shape elsewhere — that class-based API was removed in RN-tvos 0.85.x. Always read the version installed in your node_modules.

Apple TV: long-press Play/Pause

import { Platform, TVEventHandler } from 'react-native';
import { useEverframe } from '@everframe/react-native';
import { useEffect } from 'react';

function useAppleTVReporterTrigger() {
  const { open } = useEverframe();
  useEffect(() => {
    if (!(Platform.isTV && Platform.OS === 'ios')) return;
    const sub = TVEventHandler.addListener((evt) => {
      if (evt.eventType !== 'longPlayPause') return;
      if (Number(evt.eventKeyAction) !== 1) return; // key-up only
      void open();
    });
    return () => sub?.remove();
  }, [open]);
}

Why longPlayPause instead of menu? .menu is reserved by Apple as the system back-navigation gesture on the Siri Remote — apps that bind it for a non-navigation purpose are App Store-rejected. Everframe's iOS SDK enforces this via ReservedKeysValidator; the same constraint applies on RN. longPlayPause is a native long-press event emitted by the OS (no JS timing required).

Android TV: KEYCODE_MENU

import { Platform, TVEventHandler } from 'react-native';
import { useEverframe } from '@everframe/react-native';
import { useEffect } from 'react';

function useAndroidTVReporterTrigger() {
  const { open } = useEverframe();
  useEffect(() => {
    if (!(Platform.isTV && Platform.OS === 'android')) return;
    const sub = TVEventHandler.addListener((evt) => {
      if (evt.eventType !== 'menu') return;
      if (Number(evt.eventKeyAction) !== 1) return; // key-up only
      void open();
    });
    return () => sub?.remove();
  }, [open]);
}

KEYCODE_MENU is mapped to eventType === 'menu' by ReactAndroidHWInputDeviceHelper (RN-tvos). It is the conventional "settings / debug menu" key on Android TV remotes.

Canonical example

The dogfood sample app at examples/react-native/src/screens/Home.tsx implements both recipes side-by-side, gated by Platform.isTV + Platform.OS. Cloned from the repo, build it with:

pnpm --filter examples-react-native ios:tv      # Apple TV simulator
pnpm --filter examples-react-native android:tv  # Android TV emulator

That example's own README covers prebuild details and the known pitfalls of the TV targets.

What about the floating bubble?

The iPhone floating-bubble overlay shipped in Phase 04 is NOT exposed via the RN bridge today (and would not make sense on TV anyway — the focus engine is the input model, not a touch-positioned overlay). On phone RN hosts, add your own host-level button. On TV hosts, use the remote recipes above.


Android reporter initialization

The Android reporter can be opened after the host activity has resumed, including apps that disable AndroidX Startup. The bridge supplies the current resumed activity, and installation and presentation run on the main thread. Repeated configuration does not register another lifecycle observer. Capture configuration remains synchronous without waiting for reporter UI initialization. A failure while opening is reported to JavaScript. Background or unavailable activities return a cancelled result with reason no_active_activity.

Activity destruction while a reporter is open resolves the pending call with { status: "cancelled", reason: "activity_destroyed" }, releases that report's frozen replay capture, and clears presenting state so the reporter can open again. Backgrounding alone does not cancel the dialog. Once Send is tapped, submission owns the result and continues independently of activity destruction; its normal delivery result is preserved. Unsaved drafts are not restored after activity recreation.

License

MIT