React Native and Expo
@atrix.dev/analytics-react-native: offline queue, background flush, screens by route pattern, native replay.
A thin layer over the shared JavaScript core, with what mobile needs on top:
- an offline queue persisted to AsyncStorage (or any store you plug in), capped at 1,000 events or 5 MB;
- a flush whenever the app goes to the background;
- screen tracking for Expo Router and React Navigation that records route patterns and never params;
- device and app context from
expo-deviceandexpo-applicationwhen they are installed; - native session replay through the iOS and Android recorders.
npx expo install @atrix.dev/analytics-react-native @react-native-async-storage/async-storage expo-device expo-application expo-cryptoEvery peer except react and react-native is optional. Without AsyncStorage, identity and the queue last
for one app run. Without expo-device/expo-application, events carry less device context.
Quickstart with Expo Router
// app/_layout.tsx
import { init, useExpoRouterScreens } from "@atrix.dev/analytics-react-native";
import { Stack, useSegments } from "expo-router";
export const atrix = init(process.env.EXPO_PUBLIC_ATRIX_KEY ?? ""); // atx_pk_eu_production_…
export default function RootLayout() {
useExpoRouterScreens(atrix, useSegments()); // records "/venues/[venueId]", never "/venues/v_123"
return <Stack />;
}import { atrix } from "./app/_layout";
atrix.track("booking_completed", { amount: 250000, currency: "PKR" }); // money: integer minor units
atrix.identify("user_42", { plan: "pro" }); // one-way; reset() on sign-out
atrix.group("club", "club_7", { name: "Padel Club" });React Navigation
import { init, trackNavigationScreens } from "@atrix.dev/analytics-react-native";
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import type { ReactNode } from "react";
const atrix = init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX");
export function App({ children }: { children: ReactNode }) {
const ref = useNavigationContainerRef();
return (
<NavigationContainer ref={ref} onReady={() => trackNavigationScreens(atrix, ref)}>
{children}
</NavigationContainer>
);
}trackNavigationScreens records the focused route's name (for example Profile) and never reads params.
For a screen you track by hand, call atrix.screen("Checkout") with a pattern, never a path with real ids.
Behaviour
- Usable at once.
init()returns straight away. Calls made while persisted state loads are buffered with their timestamps and replayed in order.await atrix.readyif you need to know when loading is done. - Batching. At 20 events, every 30 s, when the app goes to the background, and on
flush()/shutdown(). - Offline. The queue survives restarts. Retries back off and honour
Retry-After. - Identity. A UUIDv7 anonymous id per install, sessions that rotate after 30 minutes idle, one-way
identify,reset()on sign-out,group, andregisterfor super-properties. There is noalias. - Consent.
consent: "pending"holds events in memory untilsetConsent("granted")."denied"stops all network traffic and wipes stored state. - Storage. Pass your own store (MMKV, SecureStore, SQLite) as the third argument; it needs async
getItem,setItemandremoveItem.
import { createAtrix, memoryAsyncStorage } from "@atrix.dev/analytics-react-native";
const atrix = createAtrix(
"atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX",
{ flushAt: 20, propertyDenylist: ["email"], beforeSend: (e) => e },
{ storage: memoryAsyncStorage() }, // swap in an MMKV-backed store with the same three methods
);Revenue
After a purchase, link the billing customer to the user so server-side revenue webhooks are attributed to them (see Revenue):
import { init } from "@atrix.dev/analytics-react-native";
const atrix = init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX");
atrix.setRevenueCustomer("rc_app_user_id_123", { provider: "revenuecat" });Feature flags
isEnabled(key), getVariant(key), getPayload(key), getFlags(), onFlags(cb) and reloadFlags() work as
on the web: lazy load, persisted cache, bootstrap, and one $feature_flag_called per flag and value per
session. Before storage has loaded they answer from bootstrap.
Session replay
import { init, maskProps, nativeReplay } from "@atrix.dev/analytics-react-native";
import { Text, View } from "react-native";
export const atrix = init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX", {
replay: nativeReplay({ sampleRate: 0.5, blockedIdentifiers: ["chat-thread"] }),
});
export function ChatThread() {
return (
<View {...maskProps("block")}>
<Text>Messages here become one opaque box in recordings.</Text>
</View>
);
}maskProps(mode) returns { nativeID: "atrix-<mode>", collapsable: false } for any View. The SDK also
exports an <AtrixMask mode="block"> wrapper that renders the same View.
nativeReplay() drives the native wireframe recorders (the AtrixReplay Swift package and the
dev.atrix:replay Android library) through a native module that autolinks. It needs a development build,
not Expo Go, and React Native 0.75 or newer on iOS. Replay data never crosses the JavaScript bridge: the native
side walks the view tree, masks on the device, keeps chunks in a persisted outbox and uploads them itself.
Without the native module installed, replay is a silent no-op. Masking rules are in
Session replay.