Skip to content

App metrics

See how your app really performs on users’ devices: how long it takes to start and become interactive, how it handles network requests, when it crashes. @symbiote-native/app-metrics wraps expo-app-metrics (startup, frame rate, memory, network request, crash and session metrics). The functions and the Session and NetworkRequestObserver classes are shared by every adapter. AppMetricsRoot ships on all five adapters, and AppMetricsErrorBoundary on React, Vue, Solid and Svelte, each wrapping that framework’s own error-catch primitive.

OS platform Support
iOS live
Android live
Framework adapter Support
React live
Vue live
Angular live (no AppMetricsErrorBoundary yet)
Svelte live
Solid live
Terminal window
npm install @symbiote-native/app-metrics

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --app-metrics (or add --app-metrics in an existing app) installs and wires this for you - see @symbiote-native/cli.

expo-app-metrics and expo-modules-core come along as regular dependencies, pinned to exact versions. Never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

No permission string or manifest entry is needed.

Wrap your app in AppMetricsRoot. It marks the first render for the startup metric and, where the adapter has one, catches render errors below it. Then call markInteractive when the app can be used.

import { markInteractive } from '@symbiote-native/app-metrics';
import { AppMetricsRoot } from '@symbiote-native/app-metrics/react';
export default function App() {
return (
<AppMetricsRoot errorBoundaryFallback={null}>
<Home onReady={() => markInteractive({ routeName: 'Home' })} />
</AppMetricsRoot>
);
}

AppMetricsRoot.wrap(App) is a React-only helper that wraps a component for you.

AppMetricsErrorBoundary reports a caught error and renders a fallback. The fallback receives the error and a resetError function (IAppMetricsErrorBoundaryFallbackProps).

import { AppMetricsErrorBoundary } from '@symbiote-native/app-metrics/react';
<AppMetricsErrorBoundary
fallback={({ error, resetError }) => (
<button title={`Retry (${String(error)})`} onPress={resetError} />
)}
>
<RiskyScreen />
</AppMetricsErrorBoundary>
import { logEvent, reportError, setGlobalAttributes } from '@symbiote-native/app-metrics';
setGlobalAttributes({ plan: 'pro' }); // attached to everything logged from now on
logEvent('checkout_started', { severity: 'info', attributes: { items: 3 } });
reportError({
source: 'reportedByUser',
message: 'payment failed',
isFatal: false,
});

useNetworkRequestObserver watches network requests for as long as the component lives (injectNetworkRequestObserver on Angular). It takes a filter and onStarted and onCompleted callbacks:

import { useNetworkRequestObserver } from '@symbiote-native/app-metrics/react';
useNetworkRequestObserver({
filter: { hosts: ['api.example.com'], methods: ['POST'] },
onStarted: event => console.log('started', event.url),
onCompleted: event => console.log(event.statusCode, event.totalDuration),
});

On Vue, Solid and Svelte it takes a getter returning the same object. Outside a component, use the NetworkRequestObserver class directly and call release() when done.

Signature Description
markFirstRender(): void Marks the first render for the startup metric. AppMetricsRoot calls it for you
markInteractive(attributes?): void Marks the app as interactive, optionally with routeName and params
logEvent(name, options?): void Logs a named event with optional displayName, body, attributes and severity
setGlobalAttributes(attributes?): void Sets attributes attached to everything logged afterwards. null clears them
reportError(error: IReportErrorInput): void Reports an error as a js.exception log event
getMainSession(): Session The main session, which lasts for the app run
getForegroundSession(): Promise<Session | null> The current foreground session, or null
getInactiveSessions(): Promise<IDebugSession[]> Past sessions. Debug builds only
getAllCrashReports(): Promise<ICrashReport[]> Android only, debug builds only. Throws UnavailabilityError elsewhere
clearStoredEntries(): Promise<void> Clears the stored metric entries
installErrorHandler(): void Wraps the global error handler so unhandled JS errors reach reportError. Runs automatically on import
Class Description
Session A metrics session: id, type, startDate, isActive(), getEndDate(), getMetrics(), getLogs(), addMetric(metric)
NetworkRequestObserver Observes requests. new NetworkRequestObserver(filter?), setFilter(filter), addListener, removeListener, release()
Name Adapters Description
AppMetricsRoot All five Marks first render and, where supported, catches render errors
AppMetricsErrorBoundary React, Vue, Solid, Svelte Reports a caught error and renders the fallback
useNetworkRequestObserver React, Vue, Solid, Svelte Observes network requests for the component’s lifetime
injectNetworkRequestObserver Angular The same, with an inject* shape

ILogEventOptions and INetworkRequestFilter

Section titled “ILogEventOptions and INetworkRequestFilter”
Field Type Description
displayName string | null | undefined A human-readable name for the event
body string | null | undefined Free-text detail
attributes Record<string, ILogAttributeValue> | null | undefined Structured attributes: strings, numbers, booleans, arrays or objects
severity 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' Defaults to 'info'
hosts string[] | null | undefined INetworkRequestFilter: only requests to these hosts
methods string[] | null | undefined INetworkRequestFilter: only these HTTP methods
Field Type Description
source 'global' | 'errorBoundary' | 'reportedByUser' Where the error came from
message string The error message
isFatal boolean Whether the error crashed the app
type string | undefined The error type or class name
stacktrace string | undefined The JS stack trace
componentStack string | undefined The React component stack. Error-boundary captures only

The onCompleted event carries id, url, method, statusCode, networkProtocol, requestBytesSent, responseBytesReceived, errorDescription, startedAt, completedAt, totalDuration and redirects.

  • installErrorHandler runs when the package is imported. It wraps React Native’s global error handler so an unhandled JS error reaches reportError before the previous handler runs.
  • A caught error is reported separately from an uncaught one. AppMetricsErrorBoundary reports through its own path and never reaches the engine’s uncaught-error reporting, by design.
  • Vue and Solid boundaries carry no component stack. Vue’s onErrorCaptured gives only a lifecycle-phase string, so only React’s boundary forwards a real componentStack.
  • On Solid and Svelte, an uncaught error still rethrows. AppMetricsRoot without errorBoundaryFallback leaves that throw to reach the caller, the same as not wrapping the tree.
  • Angular has no AppMetricsErrorBoundary yet. Angular’s @boundary and @error template primitive is stable from @angular/core 22.2, but this repo pins ~22.0.8 because the AOT linker in newer versions asserts Babel 8, which cannot run inside Metro’s Babel 7. It ships once the catalog can move past 22.0.8.
  • getForegroundSession works on both platforms. Upstream’s comment says iOS only, but the native modules implement it on both.
  • Metrics can only be verified on a device. The headless tests fake the native module and the React layer is mounted through the repo’s Fabric-recording harness.
  • What does it collect? App startup (cold and warm launch, bundle load, time to first render), frame rate, memory and sessions, plus your own events on the same session timeline.
  • Where do the numbers go? Expo’s EAS Observe service, or any OpenTelemetry-compatible backend.
  • Why is time to interactive missing? It is only reported once you mark the app interactive yourself.
  • Debug or release? Judge startup numbers on release builds only.

Sources: Expo docs: Introduction to EAS Observe, Set up EAS Observe, Introducing Observe.

expo-app-metrics’s JS is hand-ported into this package’s core/. Session and NetworkRequestObserver are native shared-object classes re-exported straight off the native module, so there is no JS logic beyond the re-export. Each framework’s error boundary wraps that framework’s own catch primitive and shares the report-building logic through core/report-caught-error.ts. The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see the native setup guide).