Analytics and Monitoring

Track screens and events, watch crash-free rate per release, and alert when a new build makes it worse.

Syntaxposthog.capture(eventName, properties?) // posthog.screen(screenName, properties?)

Analytics answers what people do in your app. Monitoring answers whether the app is working at all. They are separate tools and a production app needs both.

Two feeds

Product analytics records named events: a screen opened, a button pressed, a purchase finished. Crash reporting records errors with a stack trace, the device, and the release they came from. You can send both to one vendor, but the questions you ask of each are different.

There is no document here and no page URL, so screen names do not arrive for free the way they do on the web. You send them. With Expo Router, usePathname() returns the active route as a string, which is a usable screen name on its own. The example below adds nothing visible to the screen: it renders the current route and sends one screen event per navigation.

The number to watch

Crash-free rate, the share of sessions that ended without a crash, is the one figure worth alerting on. The @sentry/react-native SDK sends sessions automatically; enableAutoSessionTracking defaults to true. Keep the rate per release rather than averaged over all of them, so a bad build is visible on the day it ships.

What trips people

Events fired during startup are lost if the SDK has not initialised yet. Guard with optional chaining: posthog?.capture("x") is the same thing as if (posthog) { posthog.capture("x") }. Put the guard inside the effect, never around the hook call, because a hook skipped on one render breaks React's hook order.

Example

Example · tsx
// app/_layout.tsx
import { useEffect } from "react";
import { Slot, usePathname } from "expo-router";
import { PostHogProvider, usePostHog } from "posthog-react-native";

function ScreenTracker() {
  // Both hooks are called unconditionally, on every render.
  const posthog = usePostHog();
  const pathname = usePathname(); // "/", "/settings", "/posts/42"

  useEffect(() => {
    // The guard lives INSIDE the effect, never around the hook call.
    // posthog?.screen(...) is the same as: if (posthog) { posthog.screen(...) }
    posthog?.screen(pathname);
  }, [posthog, pathname]);

  return <Slot />; // renders whichever route is active
}

export default function RootLayout() {
  return (
    <PostHogProvider
      apiKey="phc_your_project_key"
      options={{ host: "https://us.i.posthog.com" }}
    >
      <ScreenTracker />
    </PostHogProvider>
  );
}

When to use it

  • Find which step of a three-screen signup flow people abandon.
  • Get a stack trace for a crash a tester saw once and cannot reproduce.
  • Alert the team when a new release drops crash-free sessions below 99.5%.
  • Confirm a feature shipped last month is actually being opened.

More examples

Crash reporting wired into the router

The Expo Router integration names each transaction after the route, so slow screens and crashes are grouped by screen instead of landing in one bucket. Reach for this on the first day a build goes to a tester: without it a crash on someone else's phone is a message saying "it closed".

Example · tsx
// app/_layout.tsx
import { isRunningInExpoGo } from "expo";
import { Slot } from "expo-router";
import * as Sentry from "@sentry/react-native";

Sentry.init({
  dsn: "https://[email protected]/0",
  // Release health (crash-free sessions) is already on; shown for clarity.
  enableAutoSessionTracking: true,
  tracesSampleRate: 0.2, // sample performance traces, do not send all of them
  integrations: [
    Sentry.expoRouterIntegration({
      enableTimeToInitialDisplay: !isRunningInExpoGo(),
    }),
  ],
  enableNativeFramesTracking: !isRunningInExpoGo(),
});

function RootLayout() {
  return <Slot />;
}

// Sentry.wrap adds touch event tracking and screen timing around the whole app.
// It is not an error boundary: render errors still need Sentry.captureException.
export default Sentry.wrap(RootLayout);

Deciding whether a release regressed

Pure functions, so they are unit-testable and can run in the same CI job that reads your monitoring API after a staged rollout. The point of writing the threshold down is that "the rate looks a bit lower" is not a decision and 0.5 points is.

Example · ts
// lib/releaseHealth.ts
export type SessionTotals = { sessions: number; crashed: number };

export function crashFreeRate({ sessions, crashed }: SessionTotals): number {
  if (sessions === 0) return 100;
  const rate = ((sessions - crashed) / sessions) * 100;
  return Math.round(rate * 100) / 100; // two decimal places
}

// A drop larger than allowedDrop percentage points is a rollback candidate.
export function regressed(
  previous: number,
  current: number,
  allowedDrop = 0.5,
): boolean {
  return previous - current > allowedDrop;
}

console.log(crashFreeRate({ sessions: 20000, crashed: 37 }));
// Output: 99.82
console.log(regressed(99.82, 99.1));
// Output: true
console.log(regressed(99.82, 99.6));
// Output: false

A typed event catalogue

Raw capture() calls take any string, so a single typo creates a second event that quietly splits your funnel in half, and you find out weeks later. The union makes the misspelling a build failure instead.

Example · ts
// lib/track.ts
type PostHogLike = {
  capture: (event: string, properties?: Record<string, unknown>) => void;
};

// One place that lists every event and the shape of its properties.
type Events = {
  screen_view: { path: string };
  checkout_started: { cartValue: number; currency: "USD" | "EUR" };
  checkout_failed: { reason: string };
};

export function track<K extends keyof Events>(
  client: PostHogLike | undefined,
  event: K,
  properties: Events[K],
): void {
  client?.capture(event, properties);
}

// track(client, "checkout_strated", { ... })  -> compile error, typo caught
// track(client, "checkout_started", { cartValue: 42, currency: "USD" }) -> ok

Discussion

  • Be the first to comment on this lesson.