atrixANALYTICS
SDKs

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-device and expo-application when 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-crypto

Every 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.ready if 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, and register for super-properties. There is no alias.
  • Consent. consent: "pending" holds events in memory until setConsent("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, setItem and removeItem.
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.

On this page