Remove old PostHogAnalytics singleton and replace with guide-based two-file architecture: AnalyticsManager (singleton wrapper with super properties, session replay, opt-out, subscription funnel) and AnalyticsEvent (type-safe enum with associated values). Key changes: - New API key, self-hosted analytics endpoint - All 19 events ported to type-safe AnalyticsEvent enum - Screen tracking via AnalyticsManager.Screen enum + SwiftUI modifier - Remove all identify() calls — fully anonymous analytics - Add lifecycle hooks: flush on background, update super properties on foreground - Add privacy opt-out toggle in Settings - Subscription funnel methods ready for IAP integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
31 KiB
PostHog iOS Integration Guide
A drop-in reference for integrating PostHog analytics into any iOS/SwiftUI app. Derived from production implementations across multiple shipped apps.
Table of Contents
- Installation
- Architecture Overview
- AnalyticsManager Setup
- Event Definitions
- Screen Tracking
- App Lifecycle Integration
- Super Properties
- Subscription Funnel Tracking
- User Identification & Person Properties
- Session Recording
- Privacy & Opt-Out
- Feature Flags
- Debug Configuration
- Event Naming Conventions
- Checklist
1. Installation
Swift Package Manager (SPM)
In Xcode: File > Add Package Dependencies
https://github.com/PostHog/posthog-ios.git
- Use Up to Next Major Version (e.g.,
3.41.0) - Add
PostHogframework to your iOS app target only (not widgets, watch, or extension targets)
Verify Installation
After adding, confirm the dependency appears in your Package.resolved:
{
"identity": "posthog-ios",
"kind": "remoteSourceControl",
"location": "https://github.com/PostHog/posthog-ios.git",
"state": {
"version": "3.41.0"
}
}
Import
import PostHog
2. Architecture Overview
The integration follows a three-file architecture:
YourApp/
Core/
Analytics/
AnalyticsManager.swift // Singleton wrapper around PostHog SDK
AnalyticsEvent.swift // Type-safe event enum
Design principles:
- Single abstraction layer - All PostHog calls go through
AnalyticsManager. No directPostHogSDKcalls scattered through the codebase. - Type-safe events - A Swift enum prevents string typos, provides autocomplete, and documents all tracked events in one place.
- @MainActor singleton - Thread-safe by design.
- Privacy-first - Opt-out support, text masking, anonymous by default.
3. AnalyticsManager Setup
Create AnalyticsManager.swift:
import Foundation
import PostHog
import UIKit
@MainActor
final class AnalyticsManager {
// MARK: - Singleton
static let shared = AnalyticsManager()
private init() {}
// MARK: - Configuration
// Replace with your PostHog project API key
private static let apiKey = "phc_YOUR_API_KEY"
// PostHog Cloud: "https://us.i.posthog.com" or "https://eu.i.posthog.com"
// Self-hosted: "https://your-posthog-instance.com"
private static let host = "https://us.i.posthog.com"
private static let optOutKey = "analyticsOptedOut"
private static let sessionReplayKey = "analytics_session_replay_enabled"
private var isConfigured = false
// MARK: - ISO8601 Formatter (for date properties)
private static let iso8601Formatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f
}()
// MARK: - Setup
func configure() {
guard !isConfigured else { return }
let config = PostHogConfig(apiKey: Self.apiKey, host: Self.host)
// Auto-capture
config.captureElementInteractions = true
config.captureApplicationLifecycleEvents = true
config.captureScreenViews = true
// Session replay
config.sessionReplay = sessionReplayEnabled
config.sessionReplayConfig.maskAllTextInputs = true
config.sessionReplayConfig.maskAllImages = false
config.sessionReplayConfig.captureNetworkTelemetry = true
config.sessionReplayConfig.screenshotMode = true // Required for SwiftUI
// Respect user opt-out preference
if isOptedOut {
config.optOut = true
}
// Debug configuration
#if DEBUG
config.debug = true
config.flushAt = 1 // Flush every event immediately in debug
#endif
PostHogSDK.shared.setup(config)
isConfigured = true
// Register initial super properties
updateSuperProperties()
}
// MARK: - Event Tracking
/// Track a type-safe analytics event
func track(_ event: AnalyticsEvent) {
guard isConfigured else { return }
let (name, properties) = event.payload
#if DEBUG
print("[Analytics] \(name)", properties ?? [:])
#endif
PostHogSDK.shared.capture(name, properties: properties)
}
// MARK: - Screen Tracking
/// Track a screen view using the Screen enum
func trackScreen(_ screen: Screen, properties: [String: Any]? = nil) {
guard isConfigured else { return }
var props: [String: Any] = ["screen_name": screen.rawValue]
if let properties { props.merge(properties) { _, new in new } }
#if DEBUG
print("[Analytics] screen_viewed: \(screen.rawValue)")
#endif
PostHogSDK.shared.capture("screen_viewed", properties: props)
}
// MARK: - Flush & Reset
/// Force flush pending events (call on app background)
func flush() {
guard isConfigured else { return }
PostHogSDK.shared.flush()
}
/// Reset device identity and cached properties
func reset() {
guard isConfigured else { return }
PostHogSDK.shared.reset()
}
// MARK: - Super Properties
/// Register properties attached to every subsequent event.
/// Call on configure() and on every app foreground.
func updateSuperProperties() {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
let device = UIDevice.current.model
let osVersion = UIDevice.current.systemVersion
// Add your app-specific properties here
var props: [String: Any] = [
"app_version": version,
"build_number": build,
"device_model": device,
"os_version": osVersion,
"is_pro": false, // Replace with your subscription check
"animations_enabled": !UIAccessibility.isReduceMotionEnabled,
]
// Example: add user preferences from UserDefaults
// props["theme"] = UserDefaults.standard.string(forKey: "theme") ?? "default"
PostHogSDK.shared.register(props)
}
// MARK: - Subscription Funnel
func trackPaywallViewed(source: String) {
guard isConfigured else { return }
PostHogSDK.shared.capture("paywall_viewed", properties: ["source": source])
}
func trackPurchaseStarted(productId: String, source: String) {
guard isConfigured else { return }
PostHogSDK.shared.capture("purchase_started", properties: [
"product_id": productId,
"source": source,
])
}
func trackPurchaseCompleted(productId: String, source: String) {
guard isConfigured else { return }
PostHogSDK.shared.capture("purchase_completed", properties: [
"product_id": productId,
"source": source,
])
}
func trackPurchaseFailed(productId: String?, source: String, error: String) {
guard isConfigured else { return }
var props: [String: Any] = ["source": source, "error": error]
if let productId { props["product_id"] = productId }
PostHogSDK.shared.capture("purchase_failed", properties: props)
}
func trackPurchaseRestored(source: String) {
guard isConfigured else { return }
PostHogSDK.shared.capture("purchase_restored", properties: ["source": source])
}
// MARK: - Subscription Status (Person Properties)
func trackSubscriptionStatusObserved(
status: String,
type: String,
source: String,
isSubscribed: Bool,
hasFullAccess: Bool,
productId: String?,
willAutoRenew: Bool?,
isInGracePeriod: Bool?,
trialDaysRemaining: Int?,
expirationDate: Date?
) {
guard isConfigured else { return }
var props: [String: Any] = [
"status": status,
"type": type,
"source": source,
"is_subscribed": isSubscribed,
"has_full_access": hasFullAccess,
]
if let productId { props["product_id"] = productId }
if let willAutoRenew { props["will_auto_renew"] = willAutoRenew }
if let isInGracePeriod { props["is_in_grace_period"] = isInGracePeriod }
if let trialDaysRemaining { props["trial_days_remaining"] = trialDaysRemaining }
if let expirationDate { props["expiration_date"] = Self.iso8601Formatter.string(from: expirationDate) }
PostHogSDK.shared.capture("subscription_status_changed", properties: props)
updateSubscriptionPersonProperties(status, type: type)
}
/// Set person-level subscription properties (persist across sessions)
private func updateSubscriptionPersonProperties(_ status: String, type: String) {
guard isConfigured else { return }
PostHogSDK.shared.capture("$set", properties: [
"$set": [
"subscription_status": status,
"subscription_type": type,
],
])
}
// MARK: - Opt-Out / Opt-In
var isOptedOut: Bool {
UserDefaults.standard.bool(forKey: Self.optOutKey)
}
func optIn() {
UserDefaults.standard.set(false, forKey: Self.optOutKey)
if isConfigured {
PostHogSDK.shared.optIn()
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": true])
}
}
func optOut() {
if isConfigured {
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": false])
}
UserDefaults.standard.set(true, forKey: Self.optOutKey)
if isConfigured {
PostHogSDK.shared.optOut()
}
}
// MARK: - Session Replay Control
var sessionReplayEnabled: Bool {
get {
if UserDefaults.standard.object(forKey: Self.sessionReplayKey) == nil {
return true // Enabled by default
}
return UserDefaults.standard.bool(forKey: Self.sessionReplayKey)
}
set {
UserDefaults.standard.set(newValue, forKey: Self.sessionReplayKey)
if newValue {
PostHogSDK.shared.startSessionRecording()
} else {
PostHogSDK.shared.stopSessionRecording()
}
}
}
}
4. Event Definitions
Create AnalyticsEvent.swift as a separate file. This keeps event definitions discoverable, provides autocomplete, and prevents event name typos.
import Foundation
enum AnalyticsEvent {
// MARK: - Onboarding
case onboardingCompleted
case onboardingSkipped
// MARK: - Navigation
case tabSwitched(tab: String, previousTab: String?)
// MARK: - Core Feature (replace with your domain events)
// case itemCreated(itemId: String, type: String)
// case itemDeleted(itemId: String)
// case itemUpdated(itemId: String, field: String)
// MARK: - Settings
case themeChanged(from: String, to: String)
case animationsToggled(enabled: Bool)
case settingsReset
case analyticsToggled(enabled: Bool)
// MARK: - IAP (tracked via AnalyticsManager methods, not this enum)
// paywall_viewed, purchase_started, purchase_completed, etc.
// MARK: - Errors
case errorOccurred(domain: String, message: String, screen: String?)
// MARK: - Payload
/// Returns (eventName, properties) tuple for PostHog capture
var payload: (String, [String: Any]?) {
switch self {
// Onboarding
case .onboardingCompleted:
return ("onboarding_completed", nil)
case .onboardingSkipped:
return ("onboarding_skipped", nil)
// Navigation
case .tabSwitched(let tab, let previousTab):
var props: [String: Any] = ["tab": tab]
if let previousTab { props["previous_tab"] = previousTab }
return ("tab_switched", props)
// Settings
case .themeChanged(let from, let to):
return ("theme_changed", ["from": from, "to": to])
case .animationsToggled(let enabled):
return ("animations_toggled", ["enabled": enabled])
case .settingsReset:
return ("settings_reset", nil)
case .analyticsToggled(let enabled):
return ("analytics_toggled", ["enabled": enabled])
// Errors
case .errorOccurred(let domain, let message, let screen):
var props: [String: Any] = ["domain": domain, "message": message]
if let screen { props["screen"] = screen }
return ("error_occurred", props)
}
}
}
Usage
// Type-safe, autocomplete-friendly
AnalyticsManager.shared.track(.tabSwitched(tab: "home", previousTab: "settings"))
AnalyticsManager.shared.track(.themeChanged(from: "light", to: "dark"))
AnalyticsManager.shared.track(.errorOccurred(domain: "network", message: "timeout", screen: "home"))
Adding New Events
- Add a case to
AnalyticsEvent - Add the
payloadmapping in the switch - Call
AnalyticsManager.shared.track(.yourEvent(...))
That's it. No strings to get wrong.
5. Screen Tracking
Screen Enum
Add to AnalyticsManager.swift (or a separate file):
extension AnalyticsManager {
enum Screen: String {
case home = "home"
case settings = "settings"
case onboarding = "onboarding"
case paywall = "paywall"
case detail = "detail"
// Add your screens here
}
}
SwiftUI ViewModifier
Add to AnalyticsManager.swift:
// MARK: - SwiftUI Screen Tracking Modifier
struct ScreenTrackingModifier: ViewModifier {
let screen: AnalyticsManager.Screen
let properties: [String: Any]?
func body(content: Content) -> some View {
content.onAppear {
AnalyticsManager.shared.trackScreen(screen, properties: properties)
}
}
}
extension View {
/// Track a screen view when this view appears
func trackScreen(_ screen: AnalyticsManager.Screen, properties: [String: Any]? = nil) -> some View {
modifier(ScreenTrackingModifier(screen: screen, properties: properties))
}
}
Usage
struct SettingsView: View {
var body: some View {
List {
// ...
}
.trackScreen(.settings)
}
}
// With extra properties
struct PaywallView: View {
let source: String
var body: some View {
VStack {
// ...
}
.trackScreen(.paywall, properties: ["source": source])
}
}
Note on Auto-Capture
config.captureScreenViews = true auto-captures SwiftUI view transitions as $screen events. The manual .trackScreen() modifier gives you named, queryable screen events (screen_viewed with screen_name). Use both - auto-capture catches everything, manual tracking gives you clean data.
6. App Lifecycle Integration
Wire analytics into your @main App struct:
@main
struct YourApp: App {
@Environment(\.scenePhase) private var scenePhase
init() {
// Configure analytics early in app launch
AnalyticsManager.shared.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
// Refresh super properties (subscription status, settings may have changed)
AnalyticsManager.shared.updateSuperProperties()
case .background:
// Flush pending events before app suspends
AnalyticsManager.shared.flush()
case .inactive:
break
@unknown default:
break
}
}
}
}
Why this matters:
| Lifecycle Event | Action | Reason |
|---|---|---|
init() |
configure() |
Analytics ready before first view appears |
.active |
updateSuperProperties() |
Subscription status or settings may have changed while backgrounded |
.background |
flush() |
Ensure pending events are sent before the OS suspends the app |
7. Super Properties
Super properties are key-value pairs attached to every event automatically. Use them for segmentation dimensions you always want available.
What to Include
| Property | Source | Why |
|---|---|---|
app_version |
Bundle.main |
Correlate issues to releases |
build_number |
Bundle.main |
Distinguish TestFlight builds |
device_model |
UIDevice.current.model |
Device-specific issues |
os_version |
UIDevice.current.systemVersion |
OS-specific behavior |
is_pro |
Your IAP manager | Segment free vs paid users |
animations_enabled |
UIAccessibility.isReduceMotionEnabled |
Accessibility insights |
| App-specific settings | UserDefaults |
Understand user preferences |
Registration Pattern
// Called in configure() and on every foreground
func updateSuperProperties() {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
PostHogSDK.shared.register([
"app_version": version,
"build_number": build,
"device_model": UIDevice.current.model,
"os_version": UIDevice.current.systemVersion,
"is_pro": YourIAPManager.shared.isSubscribed,
"animations_enabled": !UIAccessibility.isReduceMotionEnabled,
// Add your app-specific properties:
// "selected_theme": UserDefaults.standard.string(forKey: "theme") ?? "default",
])
}
Rules
- Only include low-cardinality values (booleans, enums, short strings). Don't register user IDs or timestamps.
- Call
updateSuperProperties()on every foreground since values can change while backgrounded. - Use
PostHogSDK.shared.register()(notregisterOnce). You want fresh values every time.
8. Subscription Funnel Tracking
Track the full purchase funnel as discrete events. This gives you conversion rates at each step.
Funnel Events
paywall_viewed → purchase_started → purchase_completed
→ purchase_failed
→ (user cancelled)
purchase_restored (separate flow)
Implementation Pattern
In your paywall/store view:
// 1. When paywall appears
.onAppear {
AnalyticsManager.shared.trackPaywallViewed(source: "settings")
}
// 2. When user taps subscribe
func subscribe(product: Product) async {
AnalyticsManager.shared.trackPurchaseStarted(productId: product.id, source: "paywall")
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
// Verify transaction...
AnalyticsManager.shared.trackPurchaseCompleted(productId: product.id, source: "paywall")
case .userCancelled:
AnalyticsManager.shared.trackPurchaseFailed(
productId: product.id, source: "paywall", error: "user_cancelled"
)
case .pending:
AnalyticsManager.shared.trackPurchaseFailed(
productId: product.id, source: "paywall", error: "pending"
)
@unknown default:
break
}
} catch {
AnalyticsManager.shared.trackPurchaseFailed(
productId: product.id, source: "paywall", error: error.localizedDescription
)
}
}
// 3. Restore purchases
func restore() async {
// ... restore logic
AnalyticsManager.shared.trackPurchaseRestored(source: "settings")
}
Subscription Status Tracking
Track detailed subscription state on every app foreground and after purchases. This lets you build subscription dashboards.
// In your IAP/Store manager, call on foreground and after purchase events:
func trackSubscriptionAnalytics(source: String) {
AnalyticsManager.shared.trackSubscriptionStatusObserved(
status: "subscribed", // "subscribed", "free", "expired", "billing_retry", "grace_period", "revoked"
type: "monthly", // "monthly", "yearly", "none"
source: source, // "app_foreground", "purchase_success", "restore"
isSubscribed: true,
hasFullAccess: true,
productId: "com.app.monthly",
willAutoRenew: true,
isInGracePeriod: false,
trialDaysRemaining: nil,
expirationDate: Date().addingTimeInterval(30 * 24 * 60 * 60)
)
}
Source values to use consistently:
| Source | When |
|---|---|
"app_foreground" |
Status checked on foreground |
"purchase_success" |
Just completed a purchase |
"restore" |
User restored purchases |
"entitlements_refresh" |
Background entitlement check |
9. User Identification & Person Properties
Anonymous by Default
PostHog assigns a random distinctId per device. For apps without user accounts, this is sufficient. Do not call identify() unless you have authenticated users.
If You Have User Accounts
// After login - link anonymous events to this user
PostHogSDK.shared.identify(userId, userProperties: [
"email": email,
"name": name,
"created_at": signUpDate.ISO8601Format(),
])
// After logout - generate new anonymous ID
AnalyticsManager.shared.reset()
Person Properties (Without Accounts)
Use $set to attach persistent properties to the anonymous user profile:
PostHogSDK.shared.capture("$set", properties: [
"$set": [
"subscription_status": "subscribed",
"subscription_type": "yearly",
],
])
These persist across sessions and are visible in PostHog's person profiles.
10. Session Recording
Configuration
Session replay is configured during configure():
config.sessionReplay = sessionReplayEnabled // User-controllable
config.sessionReplayConfig.maskAllTextInputs = true // Privacy: mask passwords, emails, etc.
config.sessionReplayConfig.maskAllImages = false // Show images for context
config.sessionReplayConfig.captureNetworkTelemetry = true // Debug network issues
config.sessionReplayConfig.screenshotMode = true // REQUIRED for SwiftUI
Key Settings
| Setting | Recommended | Why |
|---|---|---|
maskAllTextInputs |
true |
Prevents capturing passwords, emails, sensitive text |
maskAllImages |
false |
Images provide useful visual context without PII risk |
captureNetworkTelemetry |
true |
Correlate UI issues with network failures |
screenshotMode |
true |
Required for SwiftUI. Without this, session replay won't work. |
Runtime Control
Let users toggle session recording:
// Start recording
PostHogSDK.shared.startSessionRecording()
// Stop recording
PostHogSDK.shared.stopSessionRecording()
The AnalyticsManager.sessionReplayEnabled property handles this with UserDefaults persistence.
11. Privacy & Opt-Out
Settings UI
Add a toggle in your Settings view:
private var analyticsToggle: some View {
Section {
Toggle(isOn: Binding(
get: { !AnalyticsManager.shared.isOptedOut },
set: { enabled in
if enabled {
AnalyticsManager.shared.optIn()
} else {
AnalyticsManager.shared.optOut()
}
}
)) {
Label {
VStack(alignment: .leading, spacing: 2) {
Text("Share Analytics")
.font(.body)
.foregroundStyle(.primary)
Text("Help improve \(appName) by sharing anonymous usage data")
.font(.caption)
.foregroundStyle(.secondary)
}
} icon: {
Image(systemName: "chart.bar.xaxis")
}
}
} header: {
Text("Privacy")
} footer: {
Text("No personal data is collected. Analytics are fully anonymous.")
}
}
Opt-Out Behavior
The opt-out flow is intentionally ordered:
func optOut() {
// 1. Capture the toggle event BEFORE opting out (so we know they left)
if isConfigured {
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": false])
}
// 2. Persist preference
UserDefaults.standard.set(true, forKey: Self.optOutKey)
// 3. Tell SDK to stop
if isConfigured {
PostHogSDK.shared.optOut()
}
}
func optIn() {
// 1. Persist preference
UserDefaults.standard.set(false, forKey: Self.optOutKey)
// 2. Tell SDK to resume
if isConfigured {
PostHogSDK.shared.optIn()
// 3. Capture the toggle event AFTER opting in (so we know they returned)
PostHogSDK.shared.capture("analytics_toggled", properties: ["enabled": true])
}
}
Privacy Checklist
- Analytics opt-out toggle in Settings
- Text inputs masked in session replay
- No PII in event properties (no emails, names, phone numbers)
- No PII in super properties
- Privacy policy link in Settings
- EULA link in Settings
- Opt-out preference respected on app restart (checked in
configure())
12. Feature Flags
PostHog supports feature flags for gradual rollouts and A/B testing. While the reference implementations don't use them yet, here's how to add them:
Basic Usage
// Check a boolean flag
if PostHogSDK.shared.isFeatureEnabled("new-onboarding") {
showNewOnboarding()
} else {
showLegacyOnboarding()
}
// Get a flag value (for multivariate flags)
let variant = PostHogSDK.shared.getFeatureFlag("checkout-button-color") as? String
With Loading State
Feature flags are fetched from the server. They may not be available immediately on cold start.
// Reload flags explicitly
PostHogSDK.shared.reloadFeatureFlags {
let enabled = PostHogSDK.shared.isFeatureEnabled("new-feature")
// Update UI
}
Tracking Experiments
PostHog auto-tracks $feature_flag_called when you check a flag. For explicit experiment tracking:
PostHogSDK.shared.capture("$feature_flag_called", properties: [
"$feature_flag": "experiment-name",
"$feature_flag_response": "variant-a",
])
13. Debug Configuration
DEBUG Build Settings
#if DEBUG
config.debug = true // Prints all events to Xcode console
config.flushAt = 1 // Sends every event immediately (no batching)
#endif
Console Output
With debug = true, you'll see PostHog SDK logs in the Xcode console:
[PostHog] Captured event: tab_switched {tab: "settings", previous_tab: "home"}
The custom #if DEBUG print statements in the track() method add your own formatted output:
[Analytics] tab_switched ["tab": "settings", "previous_tab": "home"]
Production Behavior
In release builds:
debugdefaults tofalse(no console logging)flushAtuses the SDK default (batched, typically 20 events or 30 seconds)- Events are queued and sent efficiently
Tip: Separate PostHog Projects
For production apps, use separate PostHog projects for development and production:
#if DEBUG
private static let apiKey = "phc_DEV_KEY"
#else
private static let apiKey = "phc_PROD_KEY"
#endif
This keeps test data out of your production dashboards.
14. Event Naming Conventions
Follow these conventions for consistency across all your apps.
Event Names
| Rule | Example | Bad Example |
|---|---|---|
| snake_case | trip_planned |
tripPlanned, Trip Planned |
| past tense for completed actions | mood_logged |
log_mood |
_tapped for UI interactions |
subscribe_tapped |
click_subscribe |
_viewed for screen/content views |
paywall_viewed |
show_paywall |
_toggled / _changed for settings |
theme_changed |
set_theme |
_enabled / _disabled for booleans |
notification_enabled |
toggle_notification |
_started / _completed / _failed for flows |
purchase_started |
begin_purchase |
Property Names
| Rule | Example |
|---|---|
| snake_case | product_id, screen_name |
| Boolean properties describe the state | enabled: true, is_pro: false |
IDs end with _id |
trip_id, stadium_id |
Counts end with _count |
stop_count, character_count |
Use source for origin tracking |
source: "settings", source: "paywall" |
| Dates use ISO8601 strings | expiration_date: "2025-01-15T00:00:00Z" |
Event Categories
Group events by domain:
enum AnalyticsEvent {
// MARK: - Onboarding
// MARK: - Navigation
// MARK: - Core Feature
// MARK: - Settings
// MARK: - Errors
}
15. Checklist
Initial Setup
- Add
posthog-iosvia SPM - Create
AnalyticsManager.swiftwith your API key and host - Create
AnalyticsEvent.swiftwith initial events - Add
Screenenum with your app's screens - Add
ScreenTrackingModifierSwiftUI extension - Call
AnalyticsManager.shared.configure()inApp.init() - Add
flush()on.backgroundscene phase - Add
updateSuperProperties()on.activescene phase
Privacy
- Analytics opt-out toggle in Settings
- Session replay text masking enabled (
maskAllTextInputs = true) screenshotMode = truefor SwiftUI- Privacy policy and EULA links in Settings
- No PII in events or super properties
- Opt-out preference checked during
configure()
Events
- Define all events as
AnalyticsEventenum cases - Follow snake_case naming convention
- Include
sourceproperty where applicable - Track error events with domain, message, and screen
- Add
.trackScreen()modifier to all major views
Subscription Tracking (if applicable)
paywall_viewedwith sourcepurchase_startedwith product ID and sourcepurchase_completedwith product ID and sourcepurchase_failedwith product ID, source, and errorpurchase_restoredwith sourcesubscription_status_changedon foreground and after purchase- Person properties set for
subscription_statusandsubscription_type
Super Properties
app_versionandbuild_numberdevice_modelandos_versionis_pro(subscription status)- App-specific user preferences
- Updated on every foreground
Debug
config.debug = truein DEBUG buildsconfig.flushAt = 1in DEBUG builds- Console logging in
track()method with#if DEBUG - Consider separate API keys for dev/prod
Auto-Capture
captureElementInteractions = truecaptureApplicationLifecycleEvents = truecaptureScreenViews = true