Observability

PostHog analytics and Sentry crash reporting — consent-gated and no-op without keys.

The template ships two observability integrations: PostHog for product analytics and Sentry for crash/error reporting. Both are Integrated, both no-op without a key, and analytics is consent-gated — a headline behavior for App Store review. This page covers how each is wired and the privacy rules that keep them review-safe.

Key files

Analytics (PostHog)

AnalyticsController wraps the PostHog SDK. The design detail that matters: start() is not called from App.init — it's called from the Consent Gate once the user accepts, because App Store reviewers flag analytics SDKs that fire before any consent UX.

To keep call sites simple, events posted before start() are buffered (capped at 50) and drained on start — so you can track(...) from anywhere without knowing whether consent has cleared yet:

func track(_ event: AnalyticsEvent) {
    guard isConfigured else { buffer(.track(event)); return }  // pre-consent → buffer
    guard isEnabled else { return }                            // no key / disabled → drop
    PostHogSDK.shared.capture(event.name, properties: event.properties)
}

Typed events

Events are a typed AnalyticsEvent enum, not stringly-typed call sites — the raw PostHog name and property dictionary live in one place:

enum AnalyticsEvent {
    case screenViewed(path: String, params: [String: String]? = nil)
    case onboardingCompleted(slideCount: Int)
    case paywallViewed(source: String?)
    case referralInviteShared(channel: String?)
    case purchaseCompleted(tier: String, period: String, source: String?)
    case purchaseRestored(source: String?)
}

Identity

The controller subscribes to the auth session stream: on sign-in it identifys with the Supabase user UUID (+ email), and on sign-out it resets — so events tie to the right person and don't bleed across accounts.

Configuration & kill-switch

KeyEffect
POSTHOG_API_KEYEnables analytics; absent → isEnabled == false, all calls no-op
POSTHOG_HOSTDefaults to https://us.i.posthog.com
POSTHOG_DISABLEDtrue → hard kill-switch even with a key present
POSTHOG_DEBUGVerbose SDK logging

Screen views are captured manually (captureScreenViews = false) so you control the paths; app-lifecycle events are on.

Crash reporting (Sentry)

SentryReporter.shared.start(dsn:authRepository:) is called once from AppEnvironment.init and no-ops when SENTRY_DSN is empty. It sets environment (development in DEBUG, else production), a 10% traces sample rate, and mirrors auth identity onto the Sentry user (cleared on sign-out).

The error bus + boundary

Errors reach Sentry through a small global bus rather than scattered try/catch:

view model  →  AppErrorBus.shared.post("message", underlyingError: err)


ErrorBoundary (mounted at RootView)  →  full-screen fallback card
        │  "Send a report"

SentryReporter.captureUserFeedback(comment)   → Sentry feedback inbox

ErrorBoundary wraps the whole app (see App Gates). Its Send a report button attaches the user's comment as Sentry user feedback; stack-trace detail is shown in DEBUG only, never in release builds. You can also call SentryReporter.shared.capture(error:) / capture(message:) directly.

dSYM upload warning is non-blocking. An Xcode "did not include a dSYM for Sentry.framework" warning during upload doesn't stop TestFlight or purchases — it only affects symbolication of some frames. See Troubleshooting.

Privacy & App Store review

  • Consent before tracking. Analytics literally cannot fire before the consent gate clears (start() gates on acceptance). Keep it that way.
  • App Privacy labels. Declare what each SDK collects when you fill out App Privacy — PostHog (usage/identifiers), Sentry (crash diagnostics), plus RevenueCat + Supabase. Mapped on the release page.
  • ATT is separate. The Permissions screen's Tracking card governs third-party IDFA tracking; first-party PostHog analytics does not depend on ATT consent.

Verify

  • Without keys, the app runs normally — no analytics, no crash reporting, no crashes.
  • With POSTHOG_API_KEY set, accept consent → events appear in PostHog; pre-consent events arrive too (buffered then drained).
  • With SENTRY_DSN set, trigger AppErrorBus.shared.post(...) → the boundary card shows, Send a report lands in the Sentry feedback inbox.
  • POSTHOG_DISABLED = true suppresses analytics even with a key.

On this page