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
| Surface | Status | Notes |
|---|---|---|
AnalyticsProvider | Integrated | Wraps PostHog, disables capture without a key, tracks screens manually, identifies signed-in users |
track() / useTrack() | Integrated | Typed event names for product analytics |
| Feature flags | Integrated | useFlag() and useFlagPayload() wrap PostHog feature flag hooks |
lib/sentry.ts | Integrated | Initializes Sentry, navigation tracing, Expo update tags, sample rates, and Expo Go safeguards |
logger | Integrated | Console logs in development; Sentry breadcrumbs and error capture in production |
| ATT helper | Native-only | expo-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=falseisAnalyticsEnabled() 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: preloadedScreenTracker 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:
| Event | Properties |
|---|---|
screen viewed | path, optional params |
onboarding completed | slideCount |
paywall viewed | optional source |
referral invite shared | optional channel |
purchase restored | optional source |
purchase completed | tier, period |
Use track() outside React components and useTrack() inside components.
Extend AnalyticsEvents first when adding a new event so payloads stay typed.
Consent and ATT
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=0Server/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 -> productionIt 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-sourcemapsIt 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:
| Service | Typical disclosure area |
|---|---|
| PostHog | Analytics, diagnostics, product interaction events, account identifiers when identified |
| Sentry | Crash diagnostics, performance traces, breadcrumbs, device/app metadata |
| RevenueCat | Purchase history and subscription state |
| Supabase | Account, 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.
