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. The key ships inside the app; inject it at build time, for example as
EXPO_PUBLIC_SA_API_KEYin Expo, rather than committing it. - For the SDK:
@react-native-async-storage/async-storage, and optionallyexpo-applicationandexpo-device, which add the vendor device id and the device model.
1. Check whether the SDK is on npm
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
npm install specvista-analytics-rn @react-native-async-storage/async-storageThe package ships TypeScript source, which Metro compiles like your own code. Call initAnalytics once, as early as possible:
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:
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:
track("checkout.completed", { plan: "pro" });
identify(user.id, { email: user.email });
reset();What the SDK does for you:
- Sends
app_openat start-up and every time the app returns to the foreground, withcoldtrue 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 andidentifydoes 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:
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 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.