Skip to content
Skip
3k

PostHog

PostHog product analytics for Skip apps on both iOS and Android.

On iOS this wraps the PostHog iOS SDK (v3.0+). On Android, the Swift code is transpiled to Kotlin via Skip Lite and wraps the PostHog Android SDK (v3.x).

Add the dependency to your Package.swift file:

let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-posthog.git", "0.0.0"..<"2.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipPostHog", package: "skip-posthog")
])
]
)

Initialize PostHog when your app starts:

import SkipPostHog
PostHogSDK.shared.setup(
PostHogConfig(apiKey: "phc_your_project_api_key")
)

For self-hosted instances:

PostHogSDK.shared.setup(
PostHogConfig(apiKey: "phc_your_project_api_key", host: "https://your-posthog-instance.com")
)
let config = PostHogConfig(apiKey: "phc_your_project_api_key")
// Event batching
config.flushAt = 20 // Send after 20 events
config.flushIntervalSeconds = 30 // Or every 30 seconds
config.maxQueueSize = 1000 // Max queued events
config.maxBatchSize = 50 // Max events per batch
// Automatic capture
config.captureApplicationLifecycleEvents = true
config.captureScreenViews = true
// Feature flags
config.sendFeatureFlagEvent = true // Emit $feature_flag_called events
config.preloadFeatureFlags = true // Preload on startup
// Person profiles
config.personProfiles = .identifiedOnly // .never, .always, or .identifiedOnly
// Session replay
config.sessionReplay = true // Enable session recording
// Surveys
config.surveys = true // Enable in-app surveys
// Debug logging
config.debug = true // Enable debug output
// Privacy
config.optOut = false // Start opted-in (default)
PostHogSDK.shared.setup(config)
// Simple event
PostHogSDK.shared.capture("button_clicked")
// Event with properties
PostHogSDK.shared.capture(
"purchase_completed",
properties: [
"product_id": "abc123",
"price": 29.99,
"currency": "USD"
]
)
// Event with user properties and groups
PostHogSDK.shared.capture(
"subscription_started",
properties: ["plan": "premium"],
userProperties: ["subscription_tier": "premium"],
groups: ["company": "acme-corp"]
)
PostHogSDK.shared.screen("Home Screen")
PostHogSDK.shared.screen(
"Product Details",
properties: ["product_id": "xyz789", "category": "Electronics"]
)
do {
try riskyOperation()
} catch {
PostHogSDK.shared.captureException(error)
PostHogSDK.shared.captureException(error, properties: ["context": "checkout"])
}
// Identify a user
PostHogSDK.shared.identify(distinctId: "user_12345")
// Identify with properties
PostHogSDK.shared.identify(
distinctId: "user_12345",
userProperties: [
"email": "user@example.com",
"plan": "premium"
],
userPropertiesSetOnce: [
"signup_date": "2024-01-15"
]
)
// Set person properties without creating an identify event
PostHogSDK.shared.setPersonProperties(
userPropertiesToSet: ["theme": "dark"],
userPropertiesToSetOnce: ["first_seen": "2024-01-15"]
)
// Alias: link anonymous IDs with identified users
PostHogSDK.shared.alias("user_12345")
// Get current IDs
let distinctId = PostHogSDK.shared.getDistinctId()
let anonymousId = PostHogSDK.shared.getAnonymousId()
// Reset when user logs out
PostHogSDK.shared.reset()
// Check if a feature is enabled
if PostHogSDK.shared.isFeatureEnabled("new_checkout_flow") {
// Show new checkout flow
}
// Get feature flag value (for multivariate flags)
if let variant = PostHogSDK.shared.getFeatureFlag("experiment") as? String {
switch variant {
case "control": // ...
case "test": // ...
default: break
}
}
// Get the full feature flag result
if let result = PostHogSDK.shared.getFeatureFlagResult("experiment") {
print("Enabled: \(result.enabled)")
print("Variant: \(result.variant ?? "none")")
print("Payload: \(result.payload ?? "none")")
}
// Get flag payload
if let payload = PostHogSDK.shared.getFeatureFlagPayload("experiment") {
// Use the JSON payload
}
// Reload flags from the server
PostHogSDK.shared.reloadFeatureFlags()
// Reload with completion callback
PostHogSDK.shared.reloadFeatureFlags {
print("Feature flags reloaded")
}
// Control whether $feature_flag_called events are sent
let enabled = PostHogSDK.shared.isFeatureEnabled("flag", sendFeatureFlagEvent: false)

Track when users see or interact with features controlled by flags:

PostHogSDK.shared.captureFeatureView(flag: "new-dashboard", flagVariant: "test")
PostHogSDK.shared.captureFeatureInteraction(flag: "new-dashboard", flagVariant: "test")

Set properties used for local feature flag evaluation:

// Person properties for local evaluation
PostHogSDK.shared.setPersonPropertiesForFlags(["plan": "premium"])
PostHogSDK.shared.resetPersonPropertiesForFlags()
// Group properties for local evaluation
PostHogSDK.shared.setGroupPropertiesForFlags("company", properties: ["industry": "tech"])
PostHogSDK.shared.resetGroupPropertiesForFlags("company")
PostHogSDK.shared.resetGroupPropertiesForFlags() // Reset all groups

Properties that are sent with every event:

// Register super properties
PostHogSDK.shared.register(["app_version": "2.1.0", "environment": "production"])
// Remove a super property
PostHogSDK.shared.unregister("environment")

Associate users with groups (companies, teams, etc.):

PostHogSDK.shared.group(type: "company", key: "acme-corp")
PostHogSDK.shared.group(
type: "company",
key: "acme-corp",
groupProperties: ["name": "Acme Corp", "plan": "enterprise"]
)
PostHogSDK.shared.startSession()
PostHogSDK.shared.endSession()
let sessionId = PostHogSDK.shared.getSessionId()
let isActive = PostHogSDK.shared.isSessionActive()
// Check if session replay is recording
if PostHogSDK.shared.isSessionReplayActive() {
print("Recording active")
}
// Start/stop recording
PostHogSDK.shared.startSessionRecording()
PostHogSDK.shared.startSessionRecording(resumeCurrent: true)
PostHogSDK.shared.stopSessionRecording()
// Opt a user out of tracking
PostHogSDK.shared.optOut()
// Check opt-out status
if PostHogSDK.shared.isOptOut() {
print("User has opted out")
}
// Opt back in
PostHogSDK.shared.optIn()
// Flush queued events immediately
PostHogSDK.shared.flush()
// Close the SDK (releases resources)
PostHogSDK.shared.close()
// Enable/disable debug logging at runtime
PostHogSDK.shared.debug(true)
Method / PropertyDescription
sharedThe singleton instance
setup(_:)Initialize with a PostHogConfig
close()Release SDK resources
flush()Send all queued events immediately
debug(_:)Enable/disable debug logging
capture(_:properties:userProperties:groups:)Track a custom event
screen(_:properties:)Track a screen view
captureException(_:properties:)Capture an error/exception
identify(distinctId:userProperties:userPropertiesSetOnce:)Identify a user
setPersonProperties(userPropertiesToSet:userPropertiesToSetOnce:)Set person properties
alias(_:)Link anonymous and identified user IDs
reset()Clear user data (on logout)
getDistinctId()Get current distinct ID
getAnonymousId()Get the anonymous ID
getSessionId()Get the current session ID
register(_:)Set super properties
unregister(_:)Remove a super property
group(type:key:groupProperties:)Associate with a group
reloadFeatureFlags()Refresh feature flags
reloadFeatureFlags(_:)Refresh with completion callback
isFeatureEnabled(_:)Check if a flag is enabled
getFeatureFlag(_:)Get a flag’s value
getFeatureFlagPayload(_:)Get a flag’s JSON payload
getFeatureFlagResult(_:)Get structured flag result
captureFeatureView(flag:flagVariant:)Track feature view
captureFeatureInteraction(flag:flagVariant:)Track feature interaction
setPersonPropertiesForFlags(_:reloadFeatureFlags:)Set local evaluation properties
setGroupPropertiesForFlags(_:properties:reloadFeatureFlags:)Set group properties for flags
startSession() / endSession()Manual session control
isSessionActive()Whether a session is active
isSessionReplayActive()Whether replay is recording
startSessionRecording(resumeCurrent:)Start session recording
stopSessionRecording()Stop session recording
optOut() / optIn() / isOptOut()Privacy controls
PropertyTypeDefaultDescription
apiKeyStringYour PostHog project API key
hostURLPostHog CloudCustom host URL
flushAtInt20Events before auto-flush
maxQueueSizeInt1000Maximum queued events
maxBatchSizeInt50Events per network request
flushIntervalSecondsTimeInterval30Auto-flush interval
sendFeatureFlagEventBooltrueEmit $feature_flag_called
preloadFeatureFlagsBooltruePreload flags on startup
captureApplicationLifecycleEventsBoolfalseTrack app lifecycle
captureScreenViewsBoolfalseAuto-track screens
captureElementInteractionsBoolfalseAutocapture (iOS only)
personProfilesPostHogPersonProfiles.identifiedOnlyProfile creation mode
sessionReplayBoolfalseEnable session recording
surveysBoolfalseEnable in-app surveys
debugBoolfalseDebug logging
optOutBoolfalseStart opted-out
reuseAnonymousIdBooltrueReuse anonymous ID on reset
setDefaultPersonPropertiesBooltrueSet default person properties
enableSwizzlingBooltrueEnable method swizzling (iOS only)

Platform-specific differences:

  • getAnonymousId() on Android returns the distinct ID (the Android SDK does not expose a separate anonymous ID).
  • captureException() on iOS uses a capture("$exception", ...) workaround because the PostHog iOS SDK’s captureException is marked @_spi and not publicly accessible.
  • captureElementInteractions (autocapture) is only available on iOS. Setting it on Android is a no-op.
  • enableSwizzling is only available on iOS. Setting it on Android is a no-op.
  • isSessionActive() on iOS is approximated by checking if a session ID exists.
  • flushIntervalSeconds is TimeInterval (Double) on iOS but Int on Android. Fractional seconds are truncated on Android.
  • Session replay and surveys may require additional setup on each platform. See the PostHog session replay docs and surveys docs.
  • remoteConfig is deprecated in newer PostHog SDK versions and is now always enabled.

This project is a Swift Package Manager module that uses the Skip plugin to build the package for both iOS and Android.

The module can be tested using the standard swift test command or by running the test target for the macOS destination in Xcode, which will run the Swift tests as well as the transpiled Kotlin JUnit tests in the Robolectric Android simulation environment.

Parity testing can be performed with skip test, which will output a table of the test results for both platforms.