# Add tracking to a React Native app

Track app opens, screen views and custom events in a React Native or Expo app on iOS and Android.

## Prerequisites

- A mobile property and its key. See [Get a project key](https://analytics.specvista.com/docs/get-a-project-key.md). The key ships inside the app; inject it at build time, for example as `EXPO_PUBLIC_SA_API_KEY` in Expo, rather than committing it.
- For the SDK: `@react-native-async-storage/async-storage`, and optionally `expo-application` and `expo-device`, which add the vendor device id and the device model.

## 1. Check whether the SDK is on npm

```bash
npm view specvista-analytics-rn version
```

- If it prints a version, follow step 2a.
- If it fails with `E404`, the SDK is not published yet. Follow step 2b.

## 2a. With the SDK

```bash
npm install specvista-analytics-rn @react-native-async-storage/async-storage
```

The package ships TypeScript source, which Metro compiles like your own code. Call `initAnalytics` once, as early as possible:

```ts
import { initAnalytics, track, trackScreen, identify, reset } from "specvista-analytics-rn";

initAnalytics({ apiKey: process.env.EXPO_PUBLIC_SA_API_KEY!, appVersion: "1.4.0", appBuild: "42" });
```

Track screens from React Navigation. `onReady` records the first screen and `onStateChange` the rest. With the helpers from step 2b, import `trackScreen` from `./analytics` instead:

```tsx
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { useRef } from "react";
import { trackScreen } from "specvista-analytics-rn";

export function AppNavigation({ children }: { children: React.ReactNode }) {
  const navigationRef = useNavigationContainerRef();
  const current = useRef<string | undefined>(undefined);
  const record = () => {
    const name = navigationRef.getCurrentRoute()?.name;
    if (name && name !== current.current) trackScreen(name);
    current.current = name;
  };
  return (
    <NavigationContainer ref={navigationRef} onReady={record} onStateChange={record}>
      {children}
    </NavigationContainer>
  );
}
```

Custom events, sign-in and sign-out:

```ts
track("checkout.completed", { plan: "pro" });
identify(user.id, { email: user.email });
reset();
```

What the SDK does for you:

- Sends `app_open` at start-up and every time the app returns to the foreground, with `cold` true when a new session starts. Sessions end after 30 minutes in the background.
- Adds the platform, OS version, app version and build, locale, time zone and a per-install id.
- Sends `environment: "development"` from development builds (`__DEV__`). Those events are stored as internal and left out of reports, so test in a release build.
- For apps directed at children, pass `audience: "child"`: user, install and device ids are never sent and `identify` does nothing.

## 2b. Without the SDK

Save these helpers as `analytics.ts` and import them from `./analytics`. They use `fetch` and have the same names and call shapes as the SDK's `track`, `trackScreen`, `identify`, `setUserId` and `reset`, so moving to the SDK later only changes the import:

```ts
import { AppState, Platform } from "react-native";

const SA_URL = "https://analyticsapi.specvista.com/api/v1";
const SA_KEY = process.env.EXPO_PUBLIC_SA_API_KEY!;
let userId: string | undefined;

export function track(event: string, properties: Record<string, unknown> = {}): void {
  const { userId: explicitUserId, ...rest } = properties;
  fetch(`${SA_URL}/ingest`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": SA_KEY },
    body: JSON.stringify({
      event,
      source: "mobile",
      userId: (explicitUserId as string | undefined) ?? userId,
      os: Platform.OS,
      osVersion: String(Platform.Version),
      environment: __DEV__ ? "development" : "production",
      properties: rest,
    }),
  }).catch(() => {});
}

export function trackScreen(screen: string, properties: Record<string, unknown> = {}): void {
  track("screen_view", { screen, ...properties });
}

export function setUserId(id: string): void {
  userId = id;
}

export function reset(): void {
  userId = undefined;
}

export function identify(id: string, traits: Record<string, unknown> = {}): void {
  userId = id;
  const { email, name, ...rest } = traits;
  fetch(`${SA_URL}/ingest/identify`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": SA_KEY },
    body: JSON.stringify({ userId: id, email, name, traits: rest }),
  }).catch(() => {});
}

let lastState = AppState.currentState;
track("app_open", { cold: true });
AppState.addEventListener("change", (state) => {
  if (state === "active" && lastState === "background") track("app_open", { cold: false });
  lastState = state;
});
```

`screen` ends up in `properties.screen`, as with the SDK. `app_open` is only sent again after the app was in the background, not after a brief interruption such as a permission prompt. This version has no sessions or install ids; add `sessionId`, `installId`, `appVersion` and the other fields from the [API reference](https://analytics.specvista.com/docs/api.md) if you need them.

## Keys in mobile apps

A key shipped in an app can be read by anyone who unpacks it. It can only send events, never read reports. If it leaks into abuse, rotate it and release an app update.

## Common errors

| Symptom | Cause | Fix |
|---|---|---|
| Nothing in reports from the simulator or a debug build | Development builds send `environment: "development"` | Test with a release build. For one check, pass `environment: "production"` to `initAnalytics` (SDK) or change the `environment` line in `analytics.ts` (step 2b), then change it back |
| Events from App Review devices are missing | Requests from Apple's network (17.0.0.0/8) are stored as internal | Expected; they are left out on purpose |
| 401 with code `unregistered_sender` | Wrong or rotated key | Copy the key again from Properties |
| Metro cannot resolve `expo-application` or `expo-device` | Neither is installed and your Metro version does not allow optional requires | Install both, or upgrade Metro |
| `npm install specvista-analytics-rn` fails with `E404` | Not published yet | Use step 2b |

## Confirm success

Open a release build, then call `GET /api/v1/analytics/{projectId}/live-events` with a bearer token and look for `app_open` from the last hour, or open the property in the dashboard. The default user agents of React Native apps are not treated as bots.
