Skip to Content

Analytics usage

Use trackEvent to send typed product events from your screens and hooks.

Complete Analytics setup first. App code imports from @/services/analytics; the PostHog SDK is confined to the analytics service and AnalyticsProvider.

What’s already tracked

The event names and properties are defined in src/services/analytics/events.ts.

EventPropertiesWhen it is sent
onboarding_completednotifications?: 'enabled' | 'skipped'Onboarding completes and its answers are saved. notifications is absent when the app has no notifications step.
sign_up_completedmethod: 'email', needs_email_confirmation: booleanSupabase accepts the sign-up request.
sign_in_completedmethod: 'email' | 'apple' | 'google'Sign-in succeeds.
signed_outNoneThe sign-out hook completes.
account_deletedNoneThe delete-account function succeeds.
chat_message_sentmodel: string, has_image: boolean, new_thread: booleanThe chat SDK calls onFinish for a pending message.
image_generatedmodel: string, style: string, aspect_ratio: stringImage generation succeeds.
image_generation_blockedreason: 'pro_required'Image generation returns pro_required.
scan_completedNoneA scan succeeds.
ai_consent_grantedversion: numberThe user taps Allow on the AI consent card and the account records it.
ai_consent_withdrawnversion: numberThe user withdraws AI consent from Profile.
paywall_shownplacement: 'onboarding_end' | 'quota_exceeded'A paywall was on screen for the placement.
paywall_skippedplacement: 'onboarding_end' | 'quota_exceeded', reason from the table belowA placement request ended without a paywall on screen.
pro_status_changedis_pro: booleanThe RevenueCat provider observes a change in Pro status after its initial value.

Every presentPaywall call sends exactly one of paywall_shown and paywall_skipped, so placement requests are the sum of the two. reason says why nothing appeared:

ReasonSent when
unconfiguredThe RevenueCat SDK is not configured on this platform.
not_presentedRevenueCat did not present its paywall.
errorRevenueCat reported an error, or either presenter, RevenueCat or Superwall, threw.
holdoutThe user is in the Superwall campaign’s holdout group.
no_audience_matchThe user matches no Superwall campaign audience.
paywall_not_availableSuperwall had no paywall it could show.
placement_not_foundNo Superwall campaign contains the placement, or Superwall returned a result the app does not recognise.

Without Superwall, quota_exceeded opens the in-app quota sheet, which counts as shown because it is the paywall for that placement.

notifications: 'enabled' records the onboarding button the user chose. It does not confirm that the operating system granted permission.

After sign-in, SessionProvider identifies the user with their Supabase ID and email. Product events omit emails, prompts, message text, images and file paths.

Adding events

Define the event

Add a property to the existing AnalyticsEvents type:

src/services/analytics/events.ts
export type AnalyticsEvents = { // Keep the existing events. report_exported: { format: 'pdf' | 'csv' }; };

Use snake_case for event and property names. Keep user-entered content out of the properties.

Track the completed action

Call the helper after the export succeeds:

import { trackEvent } from '@/services/analytics'; trackEvent('report_exported', { format: 'pdf' });

TypeScript checks the event name and its property shape. An event with no properties uses undefined in the type and takes no second argument.

Verify

Run yarn typecheck, perform the action, then find report_exported in PostHog and check its format property.

If no event arrives, confirm that both EXPO_PUBLIC_POSTHOG_API_KEY and EXPO_PUBLIC_POSTHOG_HOST are set. A missing value disables analytics without an error.

Screen views

The app does not call analytics.screen() explicitly. To track route changes, follow Expo Router’s screen-tracking pattern  in a component mounted under the analytics provider:

import { useEffect } from 'react'; import { usePathname } from 'expo-router'; import { analytics } from '@/services/analytics'; export function ScreenTracking() { const pathname = usePathname(); useEffect(() => { analytics.screen(pathname); }, [pathname]); return null; }

Check that a route change produces one screen event. Avoid adding another tracker if you have already configured screen tracking elsewhere.

Swapping the provider

Write an adapter

Create src/services/analytics/<vendor>.adapter.ts implementing AnalyticsPort. Use posthog.adapter.ts as the example.

Select the adapter

Update src/services/analytics/analytics.ts to create your adapter when the vendor is configured and use noopAnalytics otherwise. Keep the exports in index.ts so existing imports continue to work.

Update the provider and lint rules

Replace or remove the PostHog-specific wrapper in src/provider/AnalyticsProvider.tsx. If the new vendor needs a provider, mount it there.

In eslint.config.js, update vendorOwners and the restricted SDK imports so the new SDK is confined to its integration files.

Verify

Run yarn typecheck and yarn lint. Sign in and confirm that sign_in_completed arrives in the new service with method: 'email'.

Last updated on