vexo-analytics
v1.12.0
Published
The analytics tool for React Native.
Downloads
35,361
Readme
vexo-analytics
Vexo is the all-in-one, zero-config analytics and session-replay SDK for React Native. From a single line of code you get screen & tap analytics, session replay, heatmaps, crash & ANR reporting, and performance monitoring — no instrumentation required.
What you get
Once vexo() runs, the SDK captures the following automatically — no extra code:
- Product analytics — screens, taps, and navigation, plus your own events via
customEvent(). - Session replay — native screen recording with privacy blur (opt out with
sessionReplay: false). - Heatmaps — tap heatmaps, segmentable with
setHeatmapSegment(). - Crash & ANR reporting — uncaught JS errors are reported as fatal crashes, and a watchdog flags JS-thread stalls, feeding a crash-free rate and error grouping. Report handled exceptions with
trackError(). - Performance monitoring — app-start latency, slow/frozen frame counts, and per-screen render timing (opt out with
performance: false). - Console capture —
console.log/info/warn/errorare recorded alongside the session.
Quickstart
Prerequisites
- Using React Navigation or Expo Router (for screen tracking).
- iOS: minimum deployment target 15.1.
Expo
- ✅ You can use this library with Development Builds. No config plugin is required.
- ❌ This library can't be used in the "Expo Go" app because it requires custom native code.
Getting started
Create an account here.
You'll be prompted into creating a new app, give it a cool name (you will be able to change that later) and once you submit it, you'll be given an API key.
Run
npx expo install vexo-analyticsif you are using Expo, oryarn add vexo-analytics/npm install vexo-analyticsotherwise. If you are using bare React Native, runpod installin the iOS folder.Add the following code to your app entry file (usually
index.js,App.jsor_layout.tsxif you're using Expo Router):import { vexo } from 'vexo-analytics'; // You may want to wrap this with `if (!__DEV__) { ... }` to only run Vexo in production. vexo('YOUR_API_KEY');Re-build and run your app (the
vexo-analyticspackage includes native code).Go to your app's page on Vexo and you should see your first event!
Wait, that's it? Yes! That's it. With that ease of integration you get an incredible set of features — go check them out!
API
Everything below is a named export of vexo-analytics.
| Export | Signature | What it does |
|---|---|---|
| vexo | vexo(apiKey: string, options?: VexoOptions): void | Initialize the SDK. VexoOptions = { sessionReplay?: boolean; performance?: boolean }. |
| customEvent | customEvent(name: string, args: object): void | Send a custom analytics event with arbitrary properties. |
| trackError | trackError(error: unknown, options?: { handled?: boolean }): void | Report a caught exception. Pass { handled: false } to count it against the crash-free rate. |
| identifyDevice | identifyDevice(id: string \| null): Promise<void> | Attach a stable identifier (e.g. an email) to the device; pass null to un-identify. See the PII note below. |
| setHeatmapSegment | setHeatmapSegment(value: string \| null \| undefined): void | Tag subsequent taps and heatmap screenshots with a segment label (1–64 chars). Pass null/undefined/'' to clear it. |
| trackScreen | trackScreen(name: string, properties?: object): void | Manually record a screen change. Use it for navigators auto-attach doesn't cover — react-native-navigation (Wix) and custom navigation. See Navigation integration. |
| enableTracking / disableTracking | (): Promise<void> | Turn all tracking and recording on/off at runtime; the choice persists across launches. |
| VexoMask / VexoUnmask | React components | Wrap a subtree to redact it from session replay on-device (VexoUnmask opts a child back out). See session replay masking. |
| VexoProvider | React component | Explicit navigation-wrap escape hatch — see Navigation integration. |
import { customEvent, trackError } from 'vexo-analytics';
// Custom event with properties
customEvent('checkout_completed', { total: 42.0, currency: 'USD', items: 3 });
// Report a caught exception (does not crash the app)
try {
await risky();
} catch (err) {
trackError(err); // handled by default
// trackError(err, { handled: false }); // counts against the crash-free rate
}Navigation integration & supported versions
vexo() auto-attaches to your navigation library by wrapping its container's render. All navigation integrations are optional peer dependencies — installing vexo-analytics in an app without @react-navigation/native produces no peer warnings.
| Integration | Supported | Notes |
|---|---|---|
| @react-navigation/native | 5 – 7 | Optional peer. Example app runs v7. |
| Expo Router | v1 – SDK 57 (latest) | Zero-config auto-attach patches the NavigationContainer forwardRef Expo Router renders from a private internal (expo-router/build/fork/NavigationContainer; src/fork/... for the v1/v2 legacy path). Expo Router has no public API to subscribe to route changes without a component in your tree, so this is the only zero-config hook — a CI lane (scripts/verify-expo-router.js) asserts it still resolves and is patchable on Expo SDK 53 and the latest SDK, so a bump that moves it fails our build, not your screen tracking. If auto-attach ever can't hook it, it logs one dev-mode warning; use the public path below. |
| react-native-navigation (Wix) / custom nav | manual | Not auto-attached. Report screens yourself with trackScreen(). |
| react-native | * (tested: 0.74 – latest) | New Architecture supported (TurboModule); the old architecture keeps working via interop. Every release is compiled against a floor + latest RN matrix in CI (iOS + Android). |
| react | * (tested: 18) | |
Explicit wrap (escape hatch): if auto-attach warns that it could not patch your navigation library (frozen module exports, an unsupported version, or no library at all), wrap the container yourself — this uses only public react-navigation APIs and no patching:
import { VexoProvider } from 'vexo-analytics';
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<VexoProvider apiKey="YOUR_API_KEY" container={NavigationContainer}>
<RootNavigator />
</VexoProvider>
);
}When auto-attach cannot patch a library it found, it logs a dev-mode warning and cleanly no-ops instead of crashing; with no navigation library at all, vexo() logs an error at render time.
Expo Router — public path (no private internals): auto-attach's Expo Router hook relies on a private internal (see the table above); it's CI-verified on SDK 53 and the latest SDK, but if you'd rather not depend on it, subscribe with Expo Router's public useNavigationContainerRef() in your root app/_layout.tsx and forward each route to trackScreen():
import { useEffect } from 'react';
import { Slot, useNavigationContainerRef } from 'expo-router';
import { vexo, trackScreen } from 'vexo-analytics';
vexo('YOUR_API_KEY');
export default function RootLayout() {
const navRef = useNavigationContainerRef();
useEffect(() => {
if (!navRef) return;
const report = () => {
const name = navRef.getCurrentRoute()?.name;
if (name) trackScreen(name);
};
report(); // initial screen
return navRef.addListener('state', report);
}, [navRef]);
return <Slot />;
}trackScreen() de-dupes a repeat of the current route, so this composes safely even while auto-attach is active — but drive one or the other per navigator.
Manual screen tracking (trackScreen)
For navigators auto-attach can't wrap — react-native-navigation (Wix), a custom/in-house navigator, modals, wizard steps, or WebView routes — call trackScreen() yourself on each screen change. It emits the same screen event and drives the same route pipeline (heatmaps, per-screen render timing, crash route) as the auto-attach:
import { trackScreen } from 'vexo-analytics';
// e.g. from react-native-navigation's componentDidAppear / a listener:
trackScreen('Home');
trackScreen('Checkout', { step: 2 }); // optional extra propertiesUse trackScreen() or auto-attach for a given navigator, not both — they report the same screens and would double-count. As a safeguard, trackScreen() treats a repeat of the current route as a no-op (the same dedupe auto-attach applies).
User consent & session replay
Vexo records user sessions (screens, taps, navigation). Under App Review Guideline 2.5.14, apps that record user activity must obtain explicit user consent and provide a clear visual indication that recording is taking place. Similar obligations apply under GDPR/CCPA. You, the app developer, are responsible for the consent flow; the SDK gives you the switches:
Consent-gated initialization (recommended): don't call
vexo(apiKey)until the user has consented. Nothing is recorded or sent beforevexo()runs.import { vexo, enableTracking, disableTracking } from 'vexo-analytics'; if (userHasConsented) { vexo('YOUR_API_KEY'); }Opt-out at runtime:
disableTracking()stops event collection and session recording (it also stops an in-flight recording); the choice is persisted across launches.enableTracking()turns it back on (recording resumes with the next session).Visual indication: the SDK does not render a recording indicator; show your own persistent indicator while tracking is enabled to satisfy 2.5.14.
Disabling session replay (or performance capture) in code
To keep session replay off entirely while all other tracking (events, screens, taps, crashes) keeps working, pass sessionReplay: false when initializing. Independently, pass performance: false to disable performance capture:
import { vexo } from 'vexo-analytics';
vexo('YOUR_API_KEY', { sessionReplay: false }); // no screen recording / blur
vexo('YOUR_API_KEY', { performance: false }); // no perf capture
vexo('YOUR_API_KEY', { sessionReplay: false, performance: false });Both decisions are made client-side and synchronously — no network round-trip, and no native recorder (or privacy blur) work is ever started, so sessionReplay: false is also the switch to reach for in performance-sensitive apps where recording on the New Architecture causes main-thread jank. Server-side settings can further restrict recording but can never re-enable it once the app passed false. Neither option is persisted: each vexo() call decides for that session, and omitting it (or passing true) keeps the default behavior.
Masking: independent of the whole-frame isBlurred blur, you can redact individual views. Masked rects are replaced with a solid block natively, on-device, before the frame is encoded or uploaded.
Per-view: wrap a sensitive subtree in
<VexoMask>; use<VexoUnmask>to opt a child back out.import { VexoMask, VexoUnmask } from 'vexo-analytics'; <VexoMask> <CreditCardForm /> <VexoUnmask><Text>Last 4: 1234</Text></VexoUnmask> </VexoMask>Defaults: pass
maskingtovexo().maskAllTextInputsdefaults to on; the others default off. Password / secure text inputs are always masked regardless.vexo('YOUR_API_KEY', { masking: { maskAllText: true, maskAllTextInputs: true, maskAllImages: false }, });
Masking applies to the recorder only; it does not affect the whole-frame blur, which you can still keep on for screens you can't verify.
PII: identifyDevice(email) attaches the email to the device record on Vexo's servers. Only call it with the user's consent, and route deletion requests to Vexo support.
Store compliance notes
- iOS Privacy Manifest: the pod ships a
PrivacyInfo.xcprivacydeclaring its required-reason API usage (NSUserDefaults CA92.1, file timestamps C617.1) and collected data types (product interaction, device ID, user ID, crash/performance/diagnostic data; no tracking). Include Vexo's data collection in your App Store privacy nutrition label. - Android 16 KB page sizes: the SDK's native dependencies are 16 KB-aligned (Google Play requirement for updates from 2027-02-01), enforced by a Gradle version floor on
co.vexo:renderscript-intrinsics-replacement-toolkit(>= 0.8.1) plus a CI alignment check. If you build with AGP < 8.5.1, check your APK/AAB packaging (useLegacyPackaging) so aligned libraries stay uncompressed and page-aligned.
Source maps & symbolication
In release builds the JS bundle is minified/Hermes-compiled, so crash stacks
arrive as unreadable frames like at t (index.android.bundle:1:452103). Upload
the composed source map for each release and Vexo de-minifies those frames in
the crash/error UI.
1. Generate the composed source map when you build the release bundle:
# React Native CLI (Hermes)
npx react-native bundle \
--platform android --dev false --minify true \
--entry-file index.js \
--bundle-output index.android.bundle \
--sourcemap-output index.android.bundle.mapMetro 0.87+ makes source-map generation ~2x faster / half the memory, so this
is cheap. For Hermes, use the map composed with the bytecode map
(compose-source-maps), which react-native bundle produces for you.
2. Upload it with the bundled CLI (uses your app's API key):
npx vexo-upload-sourcemaps \
--platform android \
--sourcemap index.android.bundle.map \
--api-key "$VEXO_API_KEY" # or set VEXO_API_KEY
# --app-version 1.2.3 # auto-detected from app.json/package.json
# --dist <eas-update-id> # for EAS Update OTA builds (default: embedded)The map is keyed by (appVersion, platform, dist) — the same identifiers the
SDK stamps on every event (app version + the Expo update id) — so incoming
crashes match the right map automatically. No SDK code change is needed to opt
in; just upload the map for each release.
Expo / EAS Update: run the upload after eas update, passing the update's
id as --dist, so OTA releases get their own map:
eas update --branch production --message "..."
npx vexo-upload-sourcemaps --platform android \
--sourcemap dist/_expo/static/js/android/index-*.map \
--dist "$(eas update:view --json | jq -r '.id')"Native crash symbolication (dSYMs / Android mapping.txt) is a follow-up;
JS/Hermes maps are supported today.
Development
yarn test— unit tests (Jest, React Native preset).maestro test .maestro/smoke.yml— E2E smoke flow (Maestro); build and install the example app on a running emulator/simulator first. Not wired into CI yet (needs an emulator job).- CI runs on GitHub Actions (
.github/workflows/ci.yml), plus a native-compile matrix (native-build.yml) that builds the module in a real app across a React Native version range on iOS and Android. Releases and native SDK builds are documented in CONTRIBUTING.md; changes are tracked in CHANGELOG.md.
