# 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` / `error` are recorded alongside the session.

## Quickstart

**Prerequisites**

- Using [React Navigation](https://reactnavigation.org/) or [Expo Router](https://docs.expo.dev/router/introduction/) (for screen tracking).
- **iOS:** minimum deployment target **15.1**.

**Expo**

- ✅ You can use this library with [Development Builds](https://docs.expo.dev/development/introduction/). No config plugin is required.
- ❌ This library can't be used in the "Expo Go" app because it [requires custom native code](https://docs.expo.dev/workflow/customizing/).

**Getting started**

1. Create an account [here](https://www.vexo.co/sign-up).
2. 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.
3. Run `npx expo install vexo-analytics` if you are using Expo, or `yarn add vexo-analytics` / `npm install vexo-analytics` otherwise. If you are using bare React Native, run `pod install` in the iOS folder.
4. Add the following code to your app entry file (usually `index.js`, `App.js` or `_layout.tsx` if you're using Expo Router):

    ```js
    import { vexo } from 'vexo-analytics';

    // You may want to wrap this with `if (!__DEV__) { ... }` to only run Vexo in production.
    vexo('YOUR_API_KEY');
    ```

5. Re-build and run your app (the `vexo-analytics` package includes native code).
6. 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](https://docs.vexo.co/features)!

## 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](#navigation-integration--supported-versions). |
| `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](#user-consent--session-replay). |
| `VexoProvider` | React component | Explicit navigation-wrap escape hatch — see [Navigation integration](#navigation-integration--supported-versions). |

```js
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()`](#manual-screen-tracking-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:

```jsx
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()`](https://docs.expo.dev/versions/latest/sdk/router/#usenavigationcontainerref) in your root `app/_layout.tsx` and forward each route to `trackScreen()`:

```tsx
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:

```js
import { trackScreen } from 'vexo-analytics';

// e.g. from react-native-navigation's componentDidAppear / a listener:
trackScreen('Home');
trackScreen('Checkout', { step: 2 }); // optional extra properties
```

Use `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](https://developer.apple.com/app-store/review/guidelines/#software-requirements), 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 before `vexo()` runs.

    ```js
    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:

```js
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.

  ```jsx
  import { VexoMask, VexoUnmask } from 'vexo-analytics';

  <VexoMask>
    <CreditCardForm />
    <VexoUnmask><Text>Last 4: 1234</Text></VexoUnmask>
  </VexoMask>
  ```

- **Defaults:** pass `masking` to `vexo()`. `maskAllTextInputs` defaults to **on**; the others default off. Password / secure text inputs are **always** masked regardless.

  ```js
  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.xcprivacy` declaring 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:

```sh
# 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.map
```

Metro 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):

```sh
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:

```sh
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](https://docs.maestro.dev/getting-started/installing-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](CONTRIBUTING.md); changes are tracked in [CHANGELOG.md](CHANGELOG.md).
