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_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

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-storage

The 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_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:

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

SymptomCauseFix
Nothing in reports from the simulator or a debug buildDevelopment 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 missingRequests from Apple's network (17.0.0.0/8) are stored as internalExpected; they are left out on purpose
401 with code unregistered_senderWrong or rotated keyCopy the key again from Properties
Metro cannot resolve expo-application or expo-deviceNeither is installed and your Metro version does not allow optional requiresInstall both, or upgrade Metro
npm install specvista-analytics-rn fails with E404Not published yetUse 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.