Observability

Configure analytics, crash reporting, logging, and tracking permissions.

Observability is split across PostHog analytics, Sentry crash reporting, a thin logger wrapper, and the iOS App Tracking Transparency permission helper. Each service no-ops when its key is absent.

What it provides

SurfaceStatusNotes
AnalyticsProviderIntegratedWraps PostHog, disables capture without a key, tracks screens manually, identifies signed-in users
track() / useTrack()IntegratedTyped event names for product analytics
Feature flagsIntegrateduseFlag() and useFlagPayload() wrap PostHog feature flag hooks
lib/sentry.tsIntegratedInitializes Sentry, navigation tracing, Expo update tags, sample rates, and Expo Go safeguards
loggerIntegratedConsole logs in development; Sentry breadcrumbs and error capture in production
ATT helperNative-onlyexpo-tracking-transparency on iOS; web returns granted as a no-op fallback

PostHog

Configure public PostHog keys:

EXPO_PUBLIC_POSTHOG_API_KEY=phc_xxxxxxxxxxxxx
EXPO_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
EXPO_PUBLIC_POSTHOG_DISABLED=false
EXPO_PUBLIC_POSTHOG_DEBUG=false

isAnalyticsEnabled() returns true only when a key exists and EXPO_PUBLIC_POSTHOG_DISABLED !== "true". When disabled, track() logs a debug skip and does not capture.

The provider options are deliberately conservative:

autocapture screens: false
autocapture touches:  false
session replay:       false
lifecycle events:     true
feature flags:        preloaded

ScreenTracker sends route changes through posthog.screen(pathname, { path, params }). AnalyticsIdentity identifies signed-in users with their Supabase UUID and resets PostHog on sign-out.

Typed events

AnalyticsEvents currently defines:

EventProperties
screen viewedpath, optional params
onboarding completedslideCount
paywall viewedoptional source
referral invite sharedoptional channel
purchase restoredoptional source
purchase completedtier, period

Use track() outside React components and useTrack() inside components. Extend AnalyticsEvents first when adding a new event so payloads stay typed.

The legal ConsentGate blocks the app UI until the current legal version is accepted, but the current analytics implementation does not wait for that consent before mounting PostHog. If you need strict analytics consent, make PostHog enablement depend on a consent value before release.

ATT is separate. app/permissions.tsx requests tracking permission through lib/tracking-permissions.ts; on web, tracking-permissions.web.ts returns a granted no-op result. ATT permission does not replace your analytics consent policy or privacy disclosures.

Sentry

Configure the public DSN and sample rates:

EXPO_PUBLIC_SENTRY_DSN=https://example@sentry.io/123
EXPO_PUBLIC_SENTRY_DEBUG=false
EXPO_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0.2
EXPO_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=0

Server/CI values stay private:

SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_URL=https://sentry.io/
SENTRY_AUTH_TOKEN=

lib/sentry.ts calls Sentry.init() at module load. It uses the app variant from Constants.expoConfig.extra.appVariant as the runtime Sentry environment:

development -> development
preview     -> preview
production  -> production

It registers reactNavigationIntegration, disables native frame tracking in Expo Go, applies sample-rate defaults of 1 outside production and 0.2 in production, and tags Expo update metadata such as update ID, embedded-update state, update group, and debug URL when available.

app/_layout.tsx registers the navigation container and exports:

export default Sentry.wrap(RootLayout);

Sourcemaps

The package script is:

pnpm sentry:upload-sourcemaps

It runs sentry-expo-upload-sourcemaps dist. Store SENTRY_AUTH_TOKEN as an EAS secret or CI secret, not as an EXPO_PUBLIC_* value. eas.json sets SENTRY_ENVIRONMENT per profile, but the runtime code currently reads the variant from Expo config for Sentry.init({ environment }).

Logger

Use logger instead of direct production console calls:

logger.info("rating.prompt_requested", { reason });
logger.warn("notifications.channels.register_failed", { error });
logger.error("profile.save_failed", error, { userId });

In development, it prints to the console. In production, it adds Sentry breadcrumbs and captures Error instances passed to logger.error().

Privacy forms

Before store submission, map your actual usage:

ServiceTypical disclosure area
PostHogAnalytics, diagnostics, product interaction events, account identifiers when identified
SentryCrash diagnostics, performance traces, breadcrumbs, device/app metadata
RevenueCatPurchase history and subscription state
SupabaseAccount, profile, app data, uploaded files

Disable services you do not use by leaving keys blank or setting the explicit kill switch, then keep App Privacy and Data Safety forms in sync with the build you ship.

On this page