From 4012c3eaab790ad8e668d1fda160de0251e2c108 Mon Sep 17 00:00:00 2001 From: "wizard-ci-bot[bot]" <254716194+wizard-ci-bot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:10:17 +0000 Subject: [PATCH] wizard-ci: swift/hackers-ios --- .../skills/integration-swift/.posthog-wizard | 0 .../references/configuration.md | 306 +++++++++ .../references/identify-users.md | 307 +++++++++ .../integration-swift/references/ios.md | 168 +++++ .../integration-swift/references/usage.md | 641 ++++++++++++++++++ .../swift/hackers-ios/.env.example | 3 + .../swift/hackers-ios/App/AppDelegate.swift | 33 + .../swift/hackers-ios/App/ContentView.swift | 9 +- .../hackers-ios/App/NavigationStore.swift | 5 + .../App/OnboardingCoordinator.swift | 2 + .../Hackers.xcodeproj/project.pbxproj | 25 + .../xcshareddata/swiftpm/Package.resolved | 11 +- 12 files changed, 1508 insertions(+), 2 deletions(-) create mode 100644 apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/.posthog-wizard create mode 100644 apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/configuration.md create mode 100644 apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/identify-users.md create mode 100644 apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/ios.md create mode 100644 apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/usage.md create mode 100644 apps/basic-integration/swift/hackers-ios/.env.example diff --git a/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/.posthog-wizard b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/.posthog-wizard new file mode 100644 index 000000000..e69de29bb diff --git a/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/configuration.md b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/configuration.md new file mode 100644 index 000000000..b8d24addc --- /dev/null +++ b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/configuration.md @@ -0,0 +1,306 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS SDK configuration - Docs + +Copy page + +# iOS SDK configuration - Docs + +## Autocapture configuration + +You can enable or disable autocapture through the `PostHogConfig` object. + +## Tracing headers + +Use `tracingHeaders` to connect iOS network requests to backend events, errors, and LLM traces captured by a server-side PostHog SDK: + +Swift + +PostHog AI + +```swift +let configuration = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +configuration.tracingHeaders = ["api.example.com"] +PostHogSDK.shared.setup(configuration) +``` + +Hostnames are matched exactly and should not include protocols, paths, ports, or wildcard subdomains. Matching `URLSession` requests include `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` when those values are available. + +Tracing headers require method swizzling, so `configuration.enableSwizzling` must remain `true`. + +## Flush configuration + +The iOS SDK uses an internal queue to make calls fast and non-blocking. It also batches requests and flushes asynchronously, making it perfect to use in any part of your mobile app. + +You can configure how many events queue before flushing with `flushAt`. Setting this to `1` will send events immediately and will use more battery. The default is `20`. + +You can also configure the flush interval with `flushIntervalSeconds` (default `30`), after which queued events are sent regardless of how many have been gathered: + +Swift + +PostHog AI + +```swift +configuration.flushAt = 1 +configuration.flushIntervalSeconds = 30 +``` + +You can also manually flush the queue to start sending events immediately instead of waiting for the next batch: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("logged_out") +PostHogSDK.shared.flush() +``` + +Flushing is best-effort and asynchronous – it starts sending queued events in the background but doesn't wait for the request to finish, so it isn't a delivery guarantee. + +## Amending, dropping or sampling events + +Since version 3.28.0, you can provide a `BeforeSendBlock` function when initializing the SDK to amend, drop or sample events before they are sent to PostHog. + +> **⚠️ Note:** This replaces the deprecated `propertiesSanitizer` option and provides more flexibility in modifying events. You can achieve the same functionality as `propertiesSanitizer` by using a `BeforeSendBlock` that mutates the event's properties in place. + +> **🚨 Warning:** Amending and sampling events is advanced functionality that requires careful implementation. Core PostHog features may require 100% of unmodified events to function properly. We recommend only modifying or sampling your own custom events if possible, and preserving all PostHog internal events in their original form. + +### Redacting information in events + +`BeforeSendBlock` gives you one place to edit or redact information before it is sent to PostHog. For example: + +Redact URLs in event properties + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.setBeforeSend { event in + // Redact URLs + if let url = event.properties["url"] as? String { + event.properties["url"] = url.map { _ in "*" }.joined() + } + return event +} +``` + +Redact sensitive information from event properties + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.setBeforeSend { event in + // Redact sensitive information + if let email = event.properties["email"] as? String { + event.properties["email"] = email.map { _ in "*" }.joined() + } + return event +} +``` + +Drop events by event name + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.setBeforeSend { event in + // Drop all events named "Stale Event" + if event.event == "Stale Event" { + return nil + } + return event +} +``` + +Filter autocaptured screen views + +You can stop specific screens from being autocaptured by filtering them in your before-send hook. Return `null` for any `$screen` event whose `$screen_name` matches a screen you don't want to track, and it's dropped before being sent – keeping unwanted screen views out of your event log. + +Because it's just a function, you can filter however you like – an **ignorelist** (drop the screens you name), an **allowlist** (invert the check to capture only the screens you name), or any custom rule such as a name prefix, a regex, or a check against the event's properties. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +let ignoredScreens: Set = ["Splash", "Debug"] +config.setBeforeSend { event in + if event.event == "$screen", + let screenName = event.properties["$screen_name"] as? String, + ignoredScreens.contains(screenName) { + return nil + } + return event +} +``` + +### Sampling events + +Sampling lets you choose to send only a percentage of events to PostHog. It is a good way to control your costs without having to completely turn off features of the SDK. + +Sample events by event name + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.setBeforeSend { event in + // Sample 10% of Sampled Event events + if event.event == "Sampled Event" { + if Double.random(in: 0...1) < 0.1 { + event.properties["$sample_type"] = ["sampleByEvent"] + event.properties["$sample_threshold"] = 0.1 + event.properties["$sampled_events"] = ["Sampled Event"] + return event + } + return nil + } + return event +} +``` + +### Chaining multiple BeforeSendBlocks + +You can provide an array of `BeforeSendBlock` functions to be called one after the other: + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.setBeforeSend( + // First block: Drop all events named "Stale Event" + { event in + if event.event == "Stale Event" { + return nil + } + return event + }, + // Second block: Redact sensitive information + { event in + if let email = event.properties["email"] as? String { + event.properties["email"] = email.map { _ in "*" }.joined() + } + return event + } +) +``` + +**Note:** When chaining beforeSend blocks, order is important. The first block is executed first and the mutated event is passed along to the second block, and so on. If at any point in the chain the event is dropped, any subsequent blocks will not be executed. + +## Setting up app groups + +1. **Configure App Groups**: Set up an [App Group](https://developer.apple.com/documentation/xcode/configuring-app-groups) in Xcode for your main app and extension targets +2. **Configure PostHog**: Use the same App Group identifier in all targets: + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.appGroupIdentifier = "group.com.yourcompany.yourapp" +PostHogSDK.shared.setup(config) +``` + +## Method swizzling + +Method swizzling is a technique that enables the SDK to intercept and modify method calls at runtime to provide advanced features like screen view tracking, element interactions, session replay, surveys, and more. + +Method swizzling is enabled by default, but can be disabled by setting the relevant config option to `false` in the `PostHogConfig` object: + +| Feature | Description | Config option | +| --- | --- | --- | +| Screen view tracking | Automatically captures when view controllers are presented | config.captureScreenViews | +| Element interactions | Automatically tracks user interactions with UI elements | config.captureElementInteractions | +| Rage clicks | Automatically captures $rageclick events for rapid repeated taps in the same area (iOS/macCatalyst, UIKit) | config.rageClickConfig.enabled | +| Session replay | Records user sessions | config.sessionReplay | +| Surveys | Displays surveys at appropriate times | config.surveys | +| Advanced metrics tracking | Provides more precise session ID calculation and rotation by detecting user activity and idleness | N/A | + +### Disabling all method swizzling + +Since version 3.34.0, you can opt out of all swizzling using the `enableSwizzling` configuration option. When you disable swizzling, the SDK disables the features listed above. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "") +config.enableSwizzling = false +PostHogSDK.shared.setup(config) +``` + +> **Note:** When method swizzling is disabled, features that depend on it will not work even if they are individually enabled in the config. For example, if you set `config.sessionReplay = true` and `config.enableSwizzling = false`, session replay will **not** be enabled. + +### Session metrics management + +Method swizzling is particularly important for accurate [session metrics tracking](/tutorials/session-metrics.md). With swizzling enabled, the SDK can better detect user activity and idle times to provide a better session rotation. + +With swizzling disabled, the SDK only uses application open/backgrounded events to detect user activity, which can lead to a sub-optimal session calculation. + +## Custom keyboard extensions + +Custom keyboard extensions have stricter security rules than other extension types. To use PostHog in a custom keyboard, the keyboard must have [Open Access permission](https://developer.apple.com/documentation/uikit/configuring-open-access-for-a-custom-keyboard) enabled. This permission is required for network requests and write access to shared containers. + +Users must explicitly grant Open Access in **Settings > General > Keyboard > Keyboards > \[Your Keyboard\] > Allow Full Access**. + +## All configuration options + +The [`PostHogConfig` object](https://github.com/PostHog/posthog-ios/blob/main/PostHog/PostHogConfig.swift) contains several other settings you can toggle: + +| Attribute | Description | +| --- | --- | +| flushAtType: IntegerDefault: 20 (5 on tvOS) | The number of queued events that the posthog client should flush at. Setting this to 1 will not queue any events and will use more battery. | +| flushIntervalSecondsType: TimeIntervalDefault: 30 | The amount of time to wait before each tick of the flush timer, in seconds. Smaller values will make events delivered in a more real-time manner and also use more battery. A value smaller than 10 seconds will seriously degrade overall performance. | +| maxQueueSizeType: IntegerDefault: 1000 (100 on tvOS) | The maximum number of items to queue before starting to drop old ones. This should be a value greater than zero, the behavior is undefined otherwise. | +| maxBatchSizeType: IntegerDefault: 50 | Number of maximum events in a batch call. | +| maxRetriesType: IntegerDefault: 3 | Maximum number of consecutive flush attempts before the entire queue is dropped to avoid infinite retries against a permanently-broken backend (e.g. wrong API key, exhausted quota, deterministic 5xx). Increments on every retriable failure including HTTP 413 cap halving; resets on a successful 2xx response. | +| captureApplicationLifecycleEventsType: BooleanDefault: true | Whether the posthog client should automatically make a capture call for application lifecycle events, such as "Application Installed", "Application Updated" and "Application Opened". | +| captureScreenViewsType: BooleanDefault: true | Whether the posthog client should automatically make a screen call when a view controller is added to a view hierarchy. Because the underlying implementation uses method swizzling, we recommend initializing the posthog client as early as possible (before any screens are displayed), ideally during the Application delegate's applicationDidFinishLaunching method. | +| enableSwizzlingType: BooleanDefault: true | Enable method swizzling for SDK functionality that depends on it. When disabled, functionality that requires swizzling (like autocapture, screen views, session replay, surveys) will not be installed. | +| captureElementInteractionsType: BooleanDefault: false | (UIKit only) Whether the posthog client should automatically make a capture call when the user interacts with an element in a screen. | +| rageClickConfigType: ObjectDefault: .init() | (iOS/macCatalyst, UIKit) Rage click detection configuration. Includes enabled (default true), minimumTapCount (default 3), thresholdPoints (default 30), and timeoutInterval (default 1.0). Works independently of captureElementInteractions. Available in version 3.51.0+. | +| sendFeatureFlagEventType: BooleanDefault: true | Send a $feature_flag_called event when a feature flag is used automatically. | +| preloadFeatureFlagsType: BooleanDefault: true | Preload feature flags automatically. | +| evaluationContextsType: Array of StringsDefault: undefined | Evaluation context tags that constrain which feature flags are evaluated. When set, only flags with matching evaluation context tags (or no evaluation context tags) will be returned. See [evaluation contexts documentation](/docs/feature-flags/evaluation-contexts.md) for more details. Available in version 3.38.0+. The legacy parameter evaluationEnvironments (version 3.33.0+) is also supported for backward compatibility. | +| debugType: BooleanDefault: false | Logs the SDK messages to the Xcode console. | +| optOutType: BooleanDefault: false | Prevents capturing any data if enabled. | +| getAnonymousIdType: FunctionDefault: undefined | Hook that allows for modification of the default mechanism for generating anonymous id (which as of now is just random UUID v7). | +| dataModeType: EnumDefault: .any | Controls when queued data is flushed. Use .wifi to flush only on Wi-Fi; .cellular is a legacy value and behaves like .any. | +| personProfilesType: EnumDefault: .identifiedOnly | Determines the behavior for processing user profiles. | +| setDefaultPersonPropertiesType: BooleanDefault: true | Automatically set common device and app properties (such as $app_version, $os_name, and $device_type) as person properties for feature flag evaluation. See [property overrides](/docs/feature-flags/property-overrides.md) for more details. | +| sessionReplayType: BooleanDefault: false | Enable Recording of Session Replays. | +| sessionReplayConfigType: ObjectDefault: .init() | Session Replay configuration. See [Session Replay installation](/docs/session-replay/installation/ios.md) for more details. | +| tracingHeadersType: Array of StringsDefault: nil | Exact hostnames that should receive PostHog tracing headers when the SDK instruments URLSession requests. | +| errorTrackingConfigType: ObjectDefault: .init() | Error Tracking configuration. See the [error tracking docs](/docs/error-tracking.md) for more details. | +| logsType: ObjectDefault: .init() | Structured Logs configuration. See [Logs installation](/docs/logs/installation/ios.md) for more details. | +| surveysConfigType: ObjectDefault: .init() | Surveys configuration, including custom survey delegates and display language overrides. | +| urlSessionConfigurationType: URLSessionConfigurationDefault: .default | Custom URLSessionConfiguration used by the SDK for PostHog API requests. | +| appGroupIdentifierType: StringDefault: nil | The identifier of the App Group that should be used to store shared analytics data. PostHog will try to get the physical location of the App Group's shared container, otherwise fallback to the default location. | +| reuseAnonymousIdType: BooleanDefault: false | Whether the SDK should reuse the anonymous Id between user changes. When enabled, a single Id will be used for all anonymous users on this device. | +| surveysType: BooleanDefault: true | Enable Surveys. | +| setBeforeSendType: FunctionDefault: undefined | Hook that allows for amending, sampling, or dropping events before they are sent to PostHog. | +| bootstrapType: PostHogBootstrapConfigDefault: nil | Seeds identity (distinctId, isIdentifiedId) and feature-flag state (featureFlags, featureFlagPayloads) before the first /flags response. Bootstrapped identity applies to the first session; only enabled flags are served, until the first /flags response replaces them. See [SDK bootstrapping](/docs/libraries/bootstrapping.md#behavior-on-mobile-sdks). | + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/identify-users.md b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/identify-users.md new file mode 100644 index 000000000..8647dcb37 --- /dev/null +++ b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/identify-users.md @@ -0,0 +1,307 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# Identify users - Docs + +Copy page + +# Identify users - Docs + +Linking events to specific users enables you to build a full picture of how they're using your product across different sessions, devices, and platforms. + +This is straightforward to do when [capturing backend events](/docs/product-analytics/capture-events?tab=Node.js.md), as you associate events to a specific user using a `distinct_id`, which is a required argument. + +However, in the frontend of a [web](/docs/libraries/js/usage.md#capturing-events) or [mobile app](/docs/libraries/ios.md#capturing-events), a `distinct_id` is not a required argument — PostHog's SDKs will generate an anonymous `distinct_id` for you automatically and you can capture events anonymously, provided you use the appropriate [configuration](/docs/libraries/js/usage.md#capturing-anonymous-events). + +To link events to specific users, call `identify`: + +PostHog AI + +### Web + +```javascript +posthog.identify( + 'distinct_id', // Replace 'distinct_id' with your user's unique identifier + { email: 'max@hedgehogmail.com', name: 'Max Hedgehog' } // optional: set additional person properties +); +``` + +### Android + +```kotlin +PostHog.identify( + distinctId = distinctID, // Replace 'distinctID' with your user's unique identifier + // optional: set additional person properties + userProperties = mapOf( + "name" to "Max Hedgehog", + "email" to "max@hedgehogmail.com" + ) +) +``` + +### iOS + +```swift +PostHogSDK.shared.identify("distinct_id", // Replace "distinct_id" with your user's unique identifier + userProperties: ["name": "Max Hedgehog", "email": "max@hedgehogmail.com"]) // optional: set additional person properties +``` + +### React Native + +```jsx +posthog.identify('distinct_id', { // Replace "distinct_id" with your user's unique identifier + email: 'max@hedgehogmail.com', // optional: set additional person properties + name: 'Max Hedgehog' +}) +``` + +### Dart + +```dart +await Posthog().identify( + userId: 'distinct_id', // Replace "distinct_id" with your user's unique identifier + userProperties: { + 'email': 'max@hedgehogmail.com', // optional: set additional person properties + 'name': 'Max Hedgehog', + }, +); +``` + +Events captured after calling `identify` are identified events and this creates a person profile if one doesn't exist already. + +Due to the cost of processing them, anonymous events can be up to 4x cheaper than identified events, so it's recommended you only capture identified events when needed. + +## How identify works + +When a user starts browsing your website or app, PostHog automatically assigns them an **anonymous ID**, which is stored locally. + +Provided you've [configured persistence](/docs/libraries/js/persistence.md) to use cookies or `localStorage`, this enables us to track anonymous users – even across different sessions. + +By calling `identify` with a `distinct_id` of your choice (usually the user's ID in your database, or their email), you link the anonymous ID and distinct ID together. + +Thus, all past and future events made with that anonymous ID are now associated with the distinct ID. + +This enables you to do things like associate events with a user from before they log in for the first time, or associate their events across different devices or platforms. + +Using identify in the backend + +Although you can call `identify` using our backend SDKs, it is used most in frontends. This is because there is no concept of anonymous sessions in the backend SDKs, so calling `identify` only updates person profiles. + +## Best practices when using `identify` + +### 1\. Call `identify` as soon as you're able to + +In your frontend, you should call `identify` as soon as you're able to. + +Typically, this is every time your **app loads** for the first time, and directly after your **users log in**. + +This ensures that events sent during your users' sessions are correctly associated with them. + +You only need to call `identify` once per session, and you should avoid calling it multiple times unnecessarily. + +If you call `identify` multiple times with the same data without reloading the page in between, PostHog will ignore the subsequent calls. + +#### Identify users when the web SDK loads + +If your app already knows the signed-in user when you initialize the JavaScript web SDK, the [`loaded` callback](/docs/libraries/js/config.md) is a convenient place to call `identify`. This identifies the user as soon as the SDK has loaded: + +Web + +PostHog AI + +```javascript +posthog.init('', { + api_host: 'https://us.i.posthog.com', + defaults: '2026-05-30', + loaded: (posthog) => { + if (currentUser?.id) { + posthog.identify(currentUser.id, { + email: currentUser.email, + name: currentUser.name, + }) + } + }, +}) +``` + +In this example, `currentUser` represents user data already available from your authentication system. If your app loads the user asynchronously, call `posthog.identify()` as soon as that data becomes available instead. + +### 2\. Use unique strings for distinct IDs + +If two users have the same distinct ID, their data is merged and they are considered one user in PostHog. Two common ways this can happen are: + +- Your logic for generating IDs does not generate sufficiently strong IDs and you can end up with a clash where 2 users have the same ID. +- There's a bug, typo, or mistake in your code leading to most or all users being identified with generic IDs like `null`, `true`, or `distinctId`. + +PostHog also has built-in protections to stop the most common distinct ID mistakes. + +### 3\. Reset after logout + +If a user logs out on your frontend, you should call `reset()` to unlink any future events made on that device with that user. + +This is important if your users are sharing a computer, as otherwise all of those users are grouped together into a single user due to shared cookies between sessions. + +**We strongly recommend you call `reset` on logout even if you don't expect users to share a computer.** + +You can do that like so: + +PostHog AI + +### Web + +```javascript +posthog.reset() +``` + +### iOS + +```swift +PostHogSDK.shared.reset() +``` + +### Android + +```kotlin +PostHog.reset() +``` + +### React Native + +```jsx +posthog.reset() +``` + +### Dart + +```dart +await Posthog().reset(); +``` + +If you *also* want to reset the `device_id` so that the device will be considered a new device in future events, you can pass `true` as an argument: + +Web + +PostHog AI + +```javascript +posthog.reset(true) +``` + +### 4\. Person profiles and properties + +You'll notice that one of the parameters in the `identify` method is a `properties` object. + +This enables you to set [person properties](/docs/product-analytics/person-properties.md). + +Whenever possible, we recommend passing in all person properties you have available each time you call identify, as this ensures their person profile on PostHog is up to date. + +Person properties can also be set being adding a `$set` property to a event `capture` call. + +**\`$set\` and \`$set\_once\` aren't stored on events** + +These properties only tell PostHog how to update person data during ingestion — they aren't kept on the stored event, so you can't filter, break down, or query events by them. To query the values you set, use [person properties](/docs/product-analytics/person-properties.md) instead. + +See our [person properties docs](/docs/product-analytics/person-properties.md) for more details on how to work with them and best practices. + +### 5\. Use deep links between platforms + +We recommend you call `identify` [as soon as you're able](#1-call-identify-as-soon-as-youre-able), typically when a user signs up or logs in. + +This doesn't work if one or both platforms are unauthenticated. Some examples of such cases are: + +- Onboarding and signup flows before authentication. +- Unauthenticated web pages redirecting to authenticated mobile apps. +- Authenticated web apps prompting an app download. + +In these cases, you can use a [deep link](https://developer.android.com/training/app-links/deep-linking) on Android and [universal links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) on iOS to identify users. + +1. Use `posthog.get_distinct_id()` to get the current distinct ID. Even if you cannot call identify because the user is unauthenticated, this will return an anonymous distinct ID generated by PostHog. +2. Add the distinct ID to the deep link as query parameters, along with other properties like UTM parameters. +3. When the user is redirected to the app, parse the deep link and handle the following cases: + +- The mobile app is already authenticated. In this case, call [`posthog.alias()`](/docs/libraries/js/usage.md#alias) with the distinct ID from the web. This associates the two distinct IDs as a single person. +- The mobile app is unauthenticated. In this case, call [`posthog.identify()`](/docs/libraries/js/usage.md#identifying-users) with the distinct ID from the web so pre-login mobile events stay connected to the web session. When the user later logs in on mobile, call `identify()` again with your canonical user ID. + +As long as you associate the distinct IDs with `posthog.identify()` or `posthog.alias()`, you can track events generated across platforms. + +Here's an example implementation for handling deep links from web to mobile: + +PostHog AI + +### iOS + +```swift +import PostHog +class DeepLinkIdentityManager { + static let shared = DeepLinkIdentityManager() + // MARK: - Deep Link Received + func handleDeepLink(_ url: URL, isAuthenticatedOnMobile: Bool) { + guard let webDistinctId = URLComponents(url: url, resolvingAgainstBaseURL: true)? + .queryItems?.first(where: { $0.name == "ph_distinct_id" })?.value else { + return + } + if isAuthenticatedOnMobile { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHogSDK.shared.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHogSDK.shared.identify(webDistinctId) + } + } + // MARK: - Login/Signup + func handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHogSDK.shared.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + func handleLogout() { + PostHogSDK.shared.reset() + } +} +``` + +### Android + +```kotlin +import android.net.Uri +import com.posthog.PostHog +object DeepLinkIdentityManager { + // Deep Link Received + fun handleDeepLink(uri: Uri, isAuthenticatedOnMobile: Boolean) { + val webDistinctId = uri.getQueryParameter("ph_distinct_id") ?: return + if (isAuthenticatedOnMobile) { + // The mobile app already knows the current user. + // Alias the incoming web distinct ID to that user. + PostHog.alias(webDistinctId) + } else { + // Reuse the web distinct ID until login on mobile. + PostHog.identify(webDistinctId) + } + } + // Login/Signup + fun handleLogin(canonicalUserId: String) { + // Switch from the web distinct ID (or a mobile anon ID) + // to your canonical user ID. + PostHog.identify(canonicalUserId) + // Set user properties, track signup event, etc. + } + fun handleLogout() { + PostHog.reset() + } +} +``` + +## Further reading + +- [Identifying users docs](/docs/product-analytics/identify.md) +- [How person processing works](/docs/how-posthog-works/ingestion-pipeline.md#2-person-processing) +- [An introductory guide to identifying users in PostHog](/tutorials/identifying-users-guide.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/ios.md b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/ios.md new file mode 100644 index 000000000..ebf46e529 --- /dev/null +++ b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/ios.md @@ -0,0 +1,168 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS - Docs + +Copy page + +# iOS - Docs + +The PostHog iOS SDK is a library that you can use to track events, identify users, record session replays, evaluate feature flags, run experiments, build surveys, and more. + +This page shows you how to install the SDK and get started with it. If you've already installed the SDK, you can skip ahead to learn about [using the features](/docs/libraries/ios/usage.md) and [configuring the SDK](/docs/libraries/ios/configuration.md). + +## Installation + +PostHog is available through [CocoaPods](http://cocoapods.org) or you can add it as a Swift Package Manager based dependency. + +### CocoaPods + +Podfile + +PostHog AI + +```ruby +pod "PostHog", "~> 3.59.3" +``` + +### Swift Package Manager + +Add PostHog as a dependency in your Xcode project "Package Dependencies" and select the project target for your app, as appropriate. + +For a Swift Package Manager based project, add PostHog as a dependency in your `Package.swift` file's Package dependencies section: + +Package.swift + +PostHog AI + +```swift +dependencies: [ + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.59.3") +], +``` + +and then as a dependency for the Package target utilizing PostHog: + +Package.swift + +PostHog AI + +```swift +.target( + name: "myApp", + dependencies: [.product(name: "PostHog", package: "posthog-ios")]), +``` + +### Configuration + +Configuration is done through the `PostHogConfig` object. Here's a basic configuration example to get you started. + +You can find more advanced configuration options in the [configuration page](/docs/libraries/ios/configuration.md). + +## UIKit + +Swift + +PostHog AI + +```swift +import Foundation +import PostHog +import UIKit +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } +} +``` + +## SwiftUI + +Swift + +PostHog AI + +```swift +import SwiftUI +import PostHog +@main +struct YourGreatApp: App { + // Add PostHog to your app's initializer. + // If using UIApplicationDelegateAdaptor, see the UIKit tab. + init() { + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + } + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +## Identifying users + +> **Identifying users is required.** Call `posthog.identify('your-user-id')` after login to link events to a known user. This is what connects frontend event captures, [session replays](/docs/session-replay.md), [LLM traces](/docs/ai-engineering.md), and [error tracking](/docs/error-tracking.md) to the same person — and lets backend events link back too. +> +> Use a stable ID from your auth system when possible, not an email or display name. Send those as person properties instead. If your app has no other stable key, email works as a fallback if they are unique. Never a shared literal like `"anonymous"` or `"user"`, which pools many people onto one person and corrupts their data. When no ID is available at all, skip the identify and retain the anonymous distinct ID that's automatically assigned. +> +> Call `posthog.reset()` on logout, so the next person to use the browser doesn't inherit the last one's identity. +> +> See our guide on [identifying users](/docs/getting-started/identify-users.md) for how to set this up. + +## Offline behavior + +The PostHog iOS SDK will continue to capture events when the device is offline. The events are stored in a queue in the device's file storage and are flushed when the device is online. + +- The queue has a maximum size defined by `maxQueueSize` in the configuration. +- When the queue is full, the oldest event is deleted first. +- The queue is flushed only when the device is online. + +You can find the options for configuring the offline behavior in the [configuration page](/docs/libraries/ios/configuration.md#all-configuration-options). + +## Using PostHog with application extensions + +PostHog supports sharing analytics data between your main app and application extensions (such as widgets, app clips, share extensions, and custom keyboards) through App Groups. This ensures that users maintain the same identity across all parts of your app ecosystem. + +By default, each iOS app target stores its data in its own sandboxed directory. This means that if a user interacts with your main app and then uses a widget or extension, PostHog would treat them as two different anonymous users. This can lead to: + +- Inflated user counts in your analytics +- Fragmented user journeys +- Difficulty tracking feature adoption across your app ecosystem + +[Learn more about setting up app groups](/docs/libraries/ios/configuration.md#setting-up-app-groups). + +## Method swizzling + +The PostHog iOS SDK uses method swizzling to intercept and modify method calls at runtime to provide advanced features like screen view tracking, element interactions, session replay, surveys, and more. + +Method swizzling is particularly important for accurate session metrics tracking. When disabled, the SDK cannot capture optimal session metrics. + +You can learn more about configuring method swizzling in the [configuration page](/docs/libraries/ios/configuration.md#method-swizzling). + +## Push notifications + +The iOS SDK can register a device for [Workflows](/docs/workflows.md) push notifications and capture when a user opens one. For setup, including automatic and manual registration, capturing opens, and identity verification, see [Push notifications](/docs/workflows/push-notifications.md). + +## Next steps + +Now that you've installed the SDK, explore the configuration and usage options: + +- [Learn about using all of the features of PostHog with iOS SDK](/docs/libraries/ios/usage.md) +- [Learn about configuration options for the iOS SDK](/docs/libraries/ios/configuration.md) + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/usage.md b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/usage.md new file mode 100644 index 000000000..000d7644b --- /dev/null +++ b/apps/basic-integration/swift/hackers-ios/.claude/skills/integration-swift/references/usage.md @@ -0,0 +1,641 @@ +> AI agents: this is one page from PostHog's docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt + +# iOS SDK usage - Docs + +Copy page + +# iOS SDK usage - Docs + +## Capturing events + +You can send custom events using `capture`: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("user_signed_up") +``` + +> **Tip:** We recommend using a `[object] [verb]` format for your event names, where `[object]` is the entity that the behavior relates to, and `[verb]` is the behavior itself. For example, `project created`, `user signed up`, or `invite sent`. + +### Setting event properties + +Optionally, you can include additional information with the event by including a [properties](/docs/data/events.md#event-properties) object: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("user_signed_up", properties: ["login_type": "email"], userProperties: ["is_free_trial": true]) +``` + +## Autocapture + +PostHog autocapture automatically tracks the following events for you: + +- **Application Opened** – when the app is opened from a closed state or when the app comes to the foreground (e.g. from the app switcher) +- **Application Backgrounded** – when the app is sent to the background by the user +- **Application Installed** – when the app is installed +- **Application Updated** – when the app is updated +- **$screen** – when the user navigates (if using `UIViewController`) +- **$autocapture** – when the user interacts with elements in a screen (`UIKit based`) and `captureElementInteractions` is enabled +- **$rageclick** – when the user rapidly taps in the same area (iOS/macCatalyst, `UIKit based`) + +> 🚧 **Note:** `$autocapture` and `$rageclick` are captured from UIKit interactions. Some SwiftUI views use UIKit under the hood (for example, `TextField` → `UITextField` and `Toggle` → `UISwitch`), so those interactions may also be autocaptured. In other SwiftUI cases, interactions might still be captured, but element metadata (such as `$elements_chain`) may be incomplete. + +### Capturing screen views + +With [`configuration.captureScreenViews`](/docs/libraries/ios/configuration.md#all-configuration-options) set as `true`, PostHog will try to record all screen changes automatically. + +If you want to manually send a new screen capture event, use the `screen` function. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.screen("Dashboard", properties: ["fromIcon": "bottom"]) +``` + +> **Important:** While `captureScreenViews` works with both `UIKit` and `SwiftUI`, the screen names captured in `SwiftUI` may not be very meaningful as they are based on internal SwiftUI view identifiers. For `SwiftUI` applications, we recommend turning this option off and instead using the `.postHogScreenView()` view modifier (see next section) to capture screen views with meaningful names. + +> **Note:** You can use the `BeforeSendBlock` to filter or drop any undesired screen events, giving you control over which screen views are sent to PostHog. See [Amending, dropping or sampling events](/docs/libraries/ios.md#amending-dropping-or-sampling-events) for implementation examples. + +### Capturing screen views in SwiftUI + +To track a screen view in `SwiftUI`, apply the `postHogScreenView` modifier to your full-screen views. PostHog will send a `$screen` event when the `onAppear` action is executed and will infer a screen name based on the view's type. You can provide a custom name and event properties if needed. + +HomeView.swift + +PostHog AI + +```swift +// This will trigger a screen view event with $screen_name: "HomeViewContent" +struct HomeView: View { + var body: some View { + HomeViewContent() + .postHogScreenView() + } +} +// This will trigger a screen view event with $screen_name: "My Home View" and an additional event property from_button: "start" +struct HomeView: View { + var body: some View { + HomeViewContent() + .postHogScreenView("My Home View", ["from_button": "start"]) + } +} +``` + +In SwiftUI, views can range from entire screens to small UI components. Unlike UIKit, SwiftUI doesn't clearly distinguish between these levels, which makes automatic tracking of full-screen views harder. + +### Adding a custom label on autocaptured elements + +PostHog automatically captures interactions with various UI elements in your app, but these interactions are often identified by element type names (e.g., UIButton, UITextField, UILabel). + +While this provides basic tracking, it can be challenging to pinpoint specific interactions with particular elements in your analytics. To make your data more meaningful and actionable, you can assign custom labels to any autocaptured element. These labels act as descriptive identifiers, making it easier to identify, filter, and analyze events in your reports. + +**Adding a custom label in UIKit** + +To assign a custom label to a UIView, use the `postHogLabel` property: + +Swift + +PostHog AI + +```swift +let view = UIView() +view.postHogLabel = "usernameTextField" +``` + +In this example, interactions with the UITextField will be captured with an additional identifier "usernameTextField". + +**Adding a custom label in SwiftUI** + +In SwiftUI, use the `.postHogLabel(_:)` modifier instead: + +Swift + +PostHog AI + +```swift +var body: some View { + ... + TextField("username", text: $username) + .postHogLabel("usernameTextField") +} +``` + +Since SwiftUI's `TextField` uses `UITextField` under the hood, interactions with it will be autocaptured with the additional identifier "usernameTextField". + +**Example of generated analytics data** + +The generated analytics element in the examples above will have the following form: + +Swift + +PostHog AI + +```swift +text value +``` + +**Filtering for labeled autocaptured elements in reports** + +To locate and filter interactions with specific elements in PostHog reports, you can use Autocapture element filters, such as: + +- Tag Name (`UITextField` in this example) +- Text (`text value` in this example) +- CSS Selector (the generated `id` attribute in this example) + +In the examples above, we can filter for the specific text field using the CSS Selector `#usernameTextField` + +### Interaction autocapture + +Interaction autocapture records when users interact with UI elements in your app. This includes: + +- User interactions like `touch`, `swipe`, `pan`, `pinch`, `rotation`, `long_press`, `scroll` +- Control types `value_changed`, `submit`, `toggle`, `primary_action`, `menu_action`, `change` + +Interaction autocapture is **not enabled by default**. You can enable it by setting `captureElementInteractions` to `true` in the config. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.captureElementInteractions = true // Disabled by default +PostHogSDK.shared.setup(config) +``` + +### Rage click autocapture + +> **Note:** Rage click autocapture for iOS/macCatalyst is available in version 3.51.0+. + +A rage click is when a user taps an area multiple times in quick succession (e.g more than 3 taps in 1 second). + +This is captured as a `$rageclick` event. You can use this event to identify opportunities to improve your UI, since it's a good indication that users may be frustrated with your product. + +It is enabled by default (`rageClickConfig.enabled = true`). + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.rageClickConfig.enabled = true // Enabled by default +config.rageClickConfig.minimumTapCount = 3 // Optional, default is 3 +config.rageClickConfig.thresholdPoints = 30 // Optional, default is 30 +config.rageClickConfig.timeoutInterval = 1.0 // Optional, default is 1.0s +PostHogSDK.shared.setup(config) +``` + +### Autocapture configuration + +You can enable or disable autocapture through the `PostHogConfig` object. Find more details about autocapture configuration in the [configuration page](/docs/libraries/ios/configuration.md#autocapture-configuration). + +## Preventing sensitive data capture + +To exclude specific UI elements from autocapture or Session Replay, add `ph-no-capture` as either an `accessibilityLabel` or `accessibilityIdentifier`. See [privacy controls](/docs/session-replay/privacy?tab=iOS.md) for masking behavior and iOS examples. + +## Identifying users + +> We highly recommend reading our section on [Identifying users](/docs/integrate/identifying-users.md) to better understand how to correctly use this method. + +Using `identify`, you can associate events with specific users. This enables you to gain full insights as to how they're using your product across different sessions, devices, and platforms. + +An `identify` call has the following arguments: + +- `distinct_id` which uniquely identifies your user in your database + +- **userProperties:** Optional. A dictionary with key:value pairs to set the [person properties](/docs/product-analytics/person-properties.md) +- **userPropertiesSetOnce:** Optional. Similar to `userProperties`. [See the difference between `userProperties` and `userPropertiesSetOnce`](/docs/product-analytics/person-properties.md#what-is-the-difference-between-set-and-set_once) + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.identify("user_id_from_your_database", + userProperties: ["name": "Peter Griffin", "email": "peter@familyguy.com"], + userPropertiesSetOnce: ["date_of_first_log_in": "2024-03-01"]) +``` + +You should call `identify` as soon as you're able to. Typically, this is after your user logs in. This ensures that events sent during your user's sessions are correctly associated with them. + +When you call `identify`, all previously tracked anonymous events will be linked to the user. + +## Get the current user's distinct ID + +You may find it helpful to get the current user's distinct ID. For example, to check whether you've already called `identify` for a user or not. + +To do this, call `getDistinctId()`. This returns either the ID automatically generated by PostHog or the ID that has been passed by a call to `identify()`. + +## Alias + +Sometimes, you want to assign multiple distinct IDs to a single user. This is helpful when your primary distinct ID is inaccessible. For example, if a distinct ID used on the frontend is not available in your backend. + +In this case, you can use `alias` to assign another distinct ID to the same user. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.alias("alias_id") +``` + +We strongly recommend reading our docs on [alias](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) to best understand how to correctly use this method. + +## Anonymous vs identified events + +PostHog captures two types of events: [**anonymous** and **identified**](/docs/data/anonymous-vs-identified-events.md) + +**Identified events** enable you to attribute events to specific users, and attach [person properties](/docs/product-analytics/person-properties.md). They're best suited for logged-in users. + +Scenarios where you want to capture identified events are: + +- Tracking logged-in users in B2B and B2C SaaS apps +- Doing user segmented product analysis +- Growth and marketing teams wanting to analyze the *complete* conversion lifecycle + +**Anonymous events** are events without individually identifiable data. They're best suited for [web analytics](/docs/web-analytics.md) or apps where users aren't logged in. + +Scenarios where you want to capture anonymous events are: + +- Tracking a marketing website +- Content-focused sites +- B2C apps where users don't sign up or log in + +Under the hood, the key difference between identified and anonymous events is that for identified events we create a [person profile](/docs/data/persons.md) for the user, whereas for anonymous events we do not. + +> **Important:** Due to the reduced cost of processing them, anonymous events can be up to 4x cheaper than identified ones, so we recommended you only capture identified events when needed. + +### How to capture anonymous events + +The iOS SDK captures anonymous events by default. However, this may change depending on your `personProfiles` [config](/docs/libraries/ios/configuration.md#all-configuration-options) when initializing PostHog: + +1. `personProfiles: .identifiedOnly` *(recommended)* *(default)* - Anonymous events are captured by default. PostHog only captures identified events for users where [person profiles](/docs/data/persons.md) have already been created. + +2. `personProfiles: .always` - Capture identified events for all events. + +3. `personProfiles: .never` - Capture anonymous events for all events. + +For example: + +iOS + +PostHog AI + +```swift +let config = PostHogConfig( + projectToken: POSTHOG_PROJECT_TOKEN, + host: POSTHOG_HOST +) +config.personProfiles = .identifiedOnly +PostHogSDK.shared.setup(config) +``` + +### How to capture identified events + +If you've set the [`personProfiles` config](/docs/libraries/ios/configuration.md#all-configuration-options) to `.identifiedOnly` (the default option), anonymous events are captured by default. Then, to capture identified events, call any of the following functions: + +- [`identify()`](/docs/product-analytics/identify.md) +- [`alias()`](/docs/product-analytics/identify.md#alias-assigning-multiple-distinct-ids-to-the-same-user) +- [`group()`](/docs/product-analytics/group-analytics.md) + +When you call any of these functions, it creates a [person profile](/docs/data/persons.md) for the user. Once this profile is created, all subsequent events for this user will be captured as identified events. + +Alternatively, you can set `personProfiles` to `.always` to capture identified events by default. + +## Setting person properties + +To set [properties](/docs/product-analytics/person-properties.md) on your users via an event, you can leverage the event properties `userProperties` and `userPropertiesSetOnce`. + +When capturing an event, you can pass a property called `$set` as an event property, and specify its value to be an object with properties to be set on the user that will be associated with the user who triggered the event. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userProperties: ["user_property_name": "your_value"]) +``` + +`userPropertiesSetOnce` works just like `userProperties`, except that it will **only set the property if the user doesn't already have that property set**. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.capture("signed_up", properties: ["plan": "Pro++"], userPropertiesSetOnce: ["user_property_name": "your_value"]) +``` + +Use `setPersonProperties` when you want to update the current person's profile without also capturing a custom event. This sends a `$set` event to PostHog. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.setPersonProperties(userPropertiesToSet: ["plan": "Pro++"]) +PostHogSDK.shared.setPersonProperties( + userPropertiesToSet: ["plan": "Pro++"], + userPropertiesToSetOnce: ["first_seen_source": "ios"] +) +``` + +## Super properties + +Super properties are properties associated with events that are set once and then sent with every `capture` call, be it a `$screen`, or anything else. + +They are set using `PostHogSDK.shared.register`, which takes a properties object as a parameter, and they persist across sessions. + +For example, take a look at the following call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.register(["team_id": 22]) +``` + +The call above ensures that every event sent by the user will include `"team_id": 22`. This way, if you filtered events by property using `team_id = 22`, it would display all events captured on that user after the `PostHogSDK.shared.register` call, since they all include the specified Super Property. + +However, please note that this does not store properties against the User, only against their events. To store properties against the User object, you should use `PostHogSDK.shared.identify`. More information on this can be found on the [Sending User Information section](#sending-user-information). + +### Removing stored super properties + +Super properties persist across sessions so you have to explicitly remove them if they are no longer relevant. To stop sending a super property with events, you can use `PostHogSDK.shared.unregister`, like so: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.unregister("team_id") +``` + +This removes the super property and subsequent events will not include it. + +If you are doing this as part of a user logging out, you can instead simply use `PostHogSDK.shared.reset` which clears all super properties and more. + +## Reset after logout + +To reset the user's ID and anonymous ID after logout, call `reset`. See [Identifying users](/docs/product-analytics/identify.md#reset) for the shared reset guidance and iOS example. + +## Group analytics + +Group analytics allows you to associate the events for that person's session with a group (e.g. teams, organizations, etc.). See [Group Analytics](/docs/product-analytics/group-analytics.md) for iOS examples and implementation details. + +> **Note:** This is a paid feature and is not available on the open-source or free cloud plan. Learn more on the [pricing page](/pricing.md). + +## Opt out of data capture + +You can completely opt users out from data capture by default or on a per-person basis. See [Complete opt-out](/docs/product-analytics/privacy.md#complete-opt-out) for iOS examples. + +## Feature flags + +PostHog's [feature flags](/docs/feature-flags.md) enable you to safely deploy and roll back new features as well as target specific users and groups with them. + +### Boolean feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.enabled { + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Multivariate feature flags + +Swift + +PostHog AI + +```swift +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), result.variant == "variant-key" { // replace "variant-key" with the key of your variant + // Do something differently for this user + // Optional: fetch the payload from the same evaluation result + let matchedFlagPayload = result.payload +} +``` + +### Typed payloads + +If your payload is a JSON object, you can decode it into a `Decodable` type: + +Swift + +PostHog AI + +```swift +struct FlagPayload: Decodable { + let title: String +} +if let result = PostHogSDK.shared.getFeatureFlagResult("flag-key"), + let payload = result.payloadAs(FlagPayload.self) { + // Use payload.title +} +``` + +### Inspecting all feature flags + +You can inspect all currently loaded feature flags with `getAllFeatureFlags()`. It returns each flag's `key`, `enabled` state, `variant`, and `payload`, and does not send a `$feature_flag_called` event, so calling it won't affect your experiment results or flag usage analytics: + +Swift + +PostHog AI + +```swift +for flag in PostHogSDK.shared.getAllFeatureFlags() ?? [] { + print(flag.key, flag.enabled, flag.variant as Any, flag.payload as Any) +} +``` + +### Reloading feature flags + +Feature flag values are cached. If something has changed with your user and you'd like to refetch their flag values, call: + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.reloadFeatureFlags() +``` + +### Ensuring flags are loaded before usage + +Every time a user opens the app, we send a request in the background to fetch the feature flags that apply to that user. We store those flags in the storage. + +This means that for most screens, the feature flags are available immediately – **except for the first time a user visits**. + +To handle this, you can use the `didReceiveFeatureFlags` notification to wait for the feature flag request to finish: + +Swift + +PostHog AI + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + // register for `didReceiveFeatureFlags` notification before SDK initialization + NotificationCenter.default.addObserver( + self, + selector: #selector(receiveFeatureFlags), + name: PostHogSDK.didReceiveFeatureFlags, + object: nil + ) + let POSTHOG_PROJECT_TOKEN = "" + // usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' + let POSTHOG_HOST = "https://us.i.posthog.com" + let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST) + PostHogSDK.shared.setup(config) + return true + } + // The "receiveFeatureFlags" method will be called when the SDK receives the feature flags from the server. + @objc func receiveFeatureFlags() { + print("receiveFeatureFlags called") + } +} +``` + +Alternatively, you can use the completion block of the `reloadFeatureFlags(_:)` method. This allows you to execute logic immediately after the flags are reloaded: + +Swift + +PostHog AI + +```swift +// Reload feature flags and check if a specific feature is enabled +PostHogSDK.shared.reloadFeatureFlags { + if PostHogSDK.shared.isFeatureEnabled("flag-key") { + // do something + } +} +``` + +### Tracking feature usage + +To track when someone sees or interacts with a feature, use `captureFeatureView` and `captureFeatureInteraction`. + +Swift + +PostHog AI + +```swift +PostHogSDK.shared.captureFeatureView(flag: "flag-key", flagVariant: "variant-key") +PostHogSDK.shared.captureFeatureInteraction(flag: "flag-key", flagVariant: "variant-key") +``` + +### Bootstrapping flags + +Since there is a delay between initializing PostHog and fetching feature flags, feature flags are not always available immediately. This makes them unusable if you want to do something like redirecting a user to a different page based on a feature flag. + +To have your feature flags available immediately, you can initialize PostHog with precomputed values until it has had a chance to fetch them. This is called bootstrapping. After the SDK fetches feature flags from PostHog, it will use those flag values instead of bootstrapped ones. + +Set `config.bootstrap` before calling `setup()` to seed identity and flag values before the first `/flags` response (requires iOS SDK `3.66.0`+): + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.bootstrap = PostHogBootstrapConfig( + distinctId: "distinct_id_of_your_user", + isIdentifiedId: true, + featureFlags: [ + "flag-1": true, + "variant-flag": "control" + ], + featureFlagPayloads: nil +) +PostHogSDK.shared.setup(config) +``` + +- **Bootstrapped identity applies during setup.** On a fresh install, setting it before `setup()` means events captured synchronously during initialization (like `Application Installed`) carry your distinct ID instead of the SDK-generated UUID. + - An **anonymous** bootstrap (`isIdentifiedId: false`, the default) seeds the anonymous ID only when none is persisted yet. Once an anonymous ID exists on disk, or the person has been identified, the SDK ignores it. + - An **identified** bootstrap (`isIdentifiedId: true`) is for a signed-in identity available to your app (for example, from a backend session token). On a fresh install, it seeds the distinct ID, marks the person identified, and generates a separate device ID. On a returning install, a matching anonymous ID is marked identified without emitting `$identify`; a different anonymous ID is merged via `identify()` when person profiles are enabled. This emits `$identify` unless capturing is opted out. A different, already-identified person is left untouched. +- **Bootstrapped flags are served until the first `/flags` response, then replaced.** A complete `/flags` response takes over entirely, so bootstrapped-only keys don't persist past it. Only *enabled* flags are seeded: a `true` boolean or a non-empty variant string. A `false` or empty value is dropped, matching posthog-js. Seed payloads with the separate `featureFlagPayloads` option. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared on `reset()`. + +The feature-flags-loaded signal fires as soon as bootstrapped flags are applied, so startup logic can read them immediately. These SDKs don't support the `sessionID` bootstrap option. When person profiles are set to `never`, the SDK preserves a different anonymous identity instead of merging it into an identified bootstrap. + +See the [SDK bootstrapping guide](/docs/libraries/bootstrapping.md) for the cross-SDK overview. + +## Experiments (A/B tests) + +Since [experiments](/docs/experiments/start-here.md) use feature flags, the code for running an experiment is very similar to the feature flags code. See [adding experiment code](/docs/experiments/adding-experiment-code.md) for iOS examples. + +It's also possible to [run experiments without using feature flags](/docs/experiments/running-experiments-without-feature-flags.md). + +## A note about IDFA (identifier for advertisers) collection in iOS 14 + +Starting with iOS 14, Apple will further restrict apps that track users. Any references to Apple's AdSupport framework, even in strings, [will trip](https://github.com/PostHog/posthog-ios/issues/6) the App Store's static analysis. + +Hence **starting with posthog-ios version 1.2.0** we have removed all references to Apple's AdSupport framework. + +## Session replay + +> **Note:** Session replay is currently only available on iOS. For future macOS support, please follow and upvote [this GitHub issue](https://github.com/PostHog/posthog-ios/issues/200). + +To set up [session replay](/docs/session-replay/mobile.md) in your project, all you need to do is install the iOS SDK, enable "Record user sessions" in [your project settings](https://us.posthog.com/settings/project-replay) and enable the `sessionReplay` option. + +## Surveys + +[Surveys](/docs/surveys.md) launched with [popover presentation](/docs/surveys/creating-surveys.md#presentation) are automatically shown to users matching the [display conditions](/docs/surveys/creating-surveys.md#display-conditions) you set up. + +## Error tracking + +To set up error tracking in your project, see the [error tracking docs](/docs/error-tracking.md). + +## Debug mode + +If you're not seeing the expected events being captured, the feature flags being evaluated, or the surveys being shown, you can enable debug mode to see what's happening. + +You can enable debug mode by setting the `debug` option to `true` in the `PostHogConfig` object. A common pattern is to set this to `true` in development environments only for local development. + +Swift + +PostHog AI + +```swift +let config = PostHogConfig(projectToken: "", host: "https://us.i.posthog.com") +config.debug = true +PostHogSDK.shared.setup(config) +``` + +This will enable verbose logs about the inner workings of the SDK. + +You can also toggle debug by calling the `PostHogSDK.shared.debug()` method in your code. + +Swift + +PostHog AI + +```swift +// Enable debug mode +PostHogSDK.shared.debug(true) +// Disable debug mode +PostHogSDK.shared.debug(false) +``` + +### Still have questions? + +Ask PostHog AI + +### Was this page useful? + +HelpfulCould be better \ No newline at end of file diff --git a/apps/basic-integration/swift/hackers-ios/.env.example b/apps/basic-integration/swift/hackers-ios/.env.example new file mode 100644 index 000000000..e92275836 --- /dev/null +++ b/apps/basic-integration/swift/hackers-ios/.env.example @@ -0,0 +1,3 @@ +# PostHog public client configuration +POSTHOG_PROJECT_TOKEN=your_posthog_project_token +POSTHOG_HOST=https://your-posthog-host diff --git a/apps/basic-integration/swift/hackers-ios/App/AppDelegate.swift b/apps/basic-integration/swift/hackers-ios/App/AppDelegate.swift index 6e1933209..f0a42bc22 100644 --- a/apps/basic-integration/swift/hackers-ios/App/AppDelegate.swift +++ b/apps/basic-integration/swift/hackers-ios/App/AppDelegate.swift @@ -6,6 +6,7 @@ // import Data +import PostHog import Shared import UIKit @@ -13,6 +14,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + configurePostHog() + // Configure a modest shared URL cache to limit on-disk growth from image/HTTP caching // This affects system components like AsyncImage that use URLSession.shared let memoryCapacity = 64 * 1024 * 1024 // 64 MB @@ -36,4 +39,34 @@ class AppDelegate: NSObject, UIApplicationDelegate { return true } + + private func configurePostHog() { + let environment = ProcessInfo.processInfo.environment + + guard let projectToken = environment["POSTHOG_PROJECT_TOKEN"] + ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_PROJECT_TOKEN") as? String, + !projectToken.isEmpty + else { + #if DEBUG + fatalError("POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_PROJECT_TOKEN is configured") + #else + return + #endif + } + + guard let host = environment["POSTHOG_HOST"] + ?? Bundle.main.object(forInfoDictionaryKey: "POSTHOG_HOST") as? String, + !host.isEmpty + else { + #if DEBUG + fatalError("POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once POSTHOG_HOST is configured") + #else + return + #endif + } + + let config = PostHogConfig(projectToken: projectToken, host: host) + config.errorTrackingConfig.autoCapture = true + PostHogSDK.shared.setup(config) + } } diff --git a/apps/basic-integration/swift/hackers-ios/App/ContentView.swift b/apps/basic-integration/swift/hackers-ios/App/ContentView.swift index a9959bced..f0bd49912 100644 --- a/apps/basic-integration/swift/hackers-ios/App/ContentView.swift +++ b/apps/basic-integration/swift/hackers-ios/App/ContentView.swift @@ -10,11 +10,12 @@ import Comments import DesignSystem import Domain import Feed +import Foundation +import PostHog import Settings import Shared import SwiftUI import UIKit -import Foundation @MainActor struct MainContentView: View { @@ -81,9 +82,11 @@ struct MainContentView: View { currentUsername: sessionService.username, onLogin: { username, password in _ = try await sessionService.authenticate(username: username, password: password) + PostHogSDK.shared.capture("login_succeeded") }, onLogout: { sessionService.unauthenticate() + PostHogSDK.shared.capture("logout_completed") }, onShowOnboarding: { showOnboarding = true @@ -103,9 +106,11 @@ struct MainContentView: View { currentUsername: sessionService.username, onLogin: { username, password in _ = try await sessionService.authenticate(username: username, password: password) + PostHogSDK.shared.capture("login_succeeded") }, onLogout: { sessionService.unauthenticate() + PostHogSDK.shared.capture("logout_completed") }, textSize: settingsViewModel.textSize ) @@ -119,9 +124,11 @@ struct MainContentView: View { currentUsername: sessionService.username, onLogin: { username, password in _ = try await sessionService.authenticate(username: username, password: password) + PostHogSDK.shared.capture("login_succeeded") }, onLogout: { sessionService.unauthenticate() + PostHogSDK.shared.capture("logout_completed") }, onShowOnboarding: { showOnboarding = true diff --git a/apps/basic-integration/swift/hackers-ios/App/NavigationStore.swift b/apps/basic-integration/swift/hackers-ios/App/NavigationStore.swift index 627d0760c..a8f06616b 100644 --- a/apps/basic-integration/swift/hackers-ios/App/NavigationStore.swift +++ b/apps/basic-integration/swift/hackers-ios/App/NavigationStore.swift @@ -8,6 +8,7 @@ import Combine import Domain import Observation +import PostHog import Shared import SwiftUI import UIKit @@ -47,6 +48,7 @@ class NavigationStore: NavigationStoreProtocol { } func showPost(_ post: Domain.Post) { + PostHogSDK.shared.capture("post_opened", properties: ["post_id": post.id]) embeddedBrowserURL = nil detailPath.removeAll() selectedPost = post @@ -59,6 +61,7 @@ class NavigationStore: NavigationStoreProtocol { } func showPost(withId id: Int) { + PostHogSDK.shared.capture("post_opened", properties: ["post_id": id]) embeddedBrowserURL = nil detailPath.removeAll() selectedPost = nil @@ -77,10 +80,12 @@ class NavigationStore: NavigationStoreProtocol { } func showLogin() { + PostHogSDK.shared.capture("login_presented") showingLogin = true } func showSettings() { + PostHogSDK.shared.capture("settings_opened") showingSettings = true } diff --git a/apps/basic-integration/swift/hackers-ios/App/OnboardingCoordinator.swift b/apps/basic-integration/swift/hackers-ios/App/OnboardingCoordinator.swift index 15acc40c3..a149ac5f1 100644 --- a/apps/basic-integration/swift/hackers-ios/App/OnboardingCoordinator.swift +++ b/apps/basic-integration/swift/hackers-ios/App/OnboardingCoordinator.swift @@ -8,6 +8,7 @@ import Domain import Foundation import Onboarding +import PostHog import SwiftUI @MainActor @@ -30,6 +31,7 @@ final class OnboardingCoordinator { func makeOnboardingView(onDismiss: @escaping () -> Void) -> some View { Onboarding.OnboardingService.createOnboardingView { + PostHogSDK.shared.capture("onboarding_completed") self.markOnboardingShown() onDismiss() } diff --git a/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.pbxproj b/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.pbxproj index 25a264601..e7c144960 100644 --- a/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.pbxproj +++ b/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 24F39F8B18AFB1150055F8DC /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24F39F8A18AFB1150055F8DC /* CoreGraphics.framework */; }; 24F39F8D18AFB1150055F8DC /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24F39F8C18AFB1150055F8DC /* UIKit.framework */; }; DA78360FD99A4A1D9C1E759C /* Authentication in Frameworks */ = {isa = PBXBuildFile; productRef = B2F5BEA03AC34B9DB788D8E2 /* Authentication */; }; + A1B2C3D4E5F60708090A0B0C /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60708090A0B0D /* PostHog */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,6 +48,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A1B2C3D4E5F60708090A0B0F /* .env */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = .env; sourceTree = ""; }; 241387A51945A6E100D71220 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 2419D02024898DF700740184 /* HackersActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HackersActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 24F39F8518AFB1150055F8DC /* Hackers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Hackers.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -102,6 +104,7 @@ 2406B38728AD083500CB396B /* SwiftSoup in Frameworks */, 241E7B412E511BC20013EEA1 /* Data in Frameworks */, 248256FB2E59D2540068E91E /* Networking in Frameworks */, + A1B2C3D4E5F60708090A0B0C /* PostHog in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -111,6 +114,7 @@ 24F39F7C18AFB1150055F8DC = { isa = PBXGroup; children = ( + A1B2C3D4E5F60708090A0B0F /* .env */, 241E7A262E4CB2830013EEA1 /* App */, 241E7A5C2E4CB28F0013EEA1 /* Extensions */, 2432F7502E7C2F5200BA0D54 /* Assets */, @@ -192,6 +196,7 @@ 248256FD2E59D2660068E91E /* Feed */, 2482576D2E5B38190068E91E /* Onboarding */, B2F5BEA03AC34B9DB788D8E2 /* Authentication */, + A1B2C3D4E5F60708090A0B0D /* PostHog */, ); productName = Hackers2; productReference = 24F39F8518AFB1150055F8DC /* Hackers.app */; @@ -237,6 +242,7 @@ 248256FC2E59D2660068E91E /* XCLocalSwiftPackageReference "Features/Feed" */, 2482576C2E5B38190068E91E /* XCLocalSwiftPackageReference "Features/Onboarding" */, 52A3FC99203E47A9B5690643 /* XCLocalSwiftPackageReference "Features/Authentication" */, + A1B2C3D4E5F60708090A0B0E /* XCRemoteSwiftPackageReference "PostHog" */, ); productRefGroup = 24F39F8618AFB1150055F8DC /* Products */; projectDirPath = ""; @@ -492,6 +498,7 @@ }; 24F39FB218AFB1150055F8DC /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A1B2C3D4E5F60708090A0B0F /* .env */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; CLANG_ENABLE_MODULES = YES; @@ -504,6 +511,8 @@ GCC_PREFIX_HEADER = "App/Supporting Files/Hackers2-Prefix.pch"; INFOPLIST_FILE = "App/Supporting Files/Hackers-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news"; + INFOPLIST_KEY_POSTHOG_HOST = "$(POSTHOG_HOST)"; + INFOPLIST_KEY_POSTHOG_PROJECT_TOKEN = "$(POSTHOG_PROJECT_TOKEN)"; IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -523,6 +532,7 @@ }; 24F39FB318AFB1150055F8DC /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A1B2C3D4E5F60708090A0B0F /* .env */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon"; CLANG_ENABLE_MODULES = YES; @@ -535,6 +545,8 @@ GCC_PREFIX_HEADER = "App/Supporting Files/Hackers2-Prefix.pch"; INFOPLIST_FILE = "App/Supporting Files/Hackers-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news"; + INFOPLIST_KEY_POSTHOG_HOST = "$(POSTHOG_HOST)"; + INFOPLIST_KEY_POSTHOG_PROJECT_TOKEN = "$(POSTHOG_PROJECT_TOKEN)"; IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -631,6 +643,14 @@ minimumVersion = 2.0.0; }; }; + A1B2C3D4E5F60708090A0B0E /* XCRemoteSwiftPackageReference "PostHog" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PostHog/posthog-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.59.3; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -671,6 +691,11 @@ isa = XCSwiftPackageProductDependency; productName = Authentication; }; + A1B2C3D4E5F60708090A0B0D /* PostHog */ = { + isa = XCSwiftPackageProductDependency; + package = A1B2C3D4E5F60708090A0B0E /* XCRemoteSwiftPackageReference "PostHog" */; + productName = PostHog; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 24F39F7D18AFB1150055F8DC /* Project object */; diff --git a/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index aedcb4505..919bb4934 100644 --- a/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/basic-integration/swift/hackers-ios/Hackers.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "944ecf5d94500e9cadaca6ec72c8aa21820f6f136372c503cd78c667c3a9c5be", + "originHash" : "8c1fc4163eedffd94bd8664fb0ed4270742d7b7806448a31b0b28523cb8a6f31", "pins" : [ + { + "identity" : "posthog-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PostHog/posthog-ios.git", + "state" : { + "revision" : "d933f3050645b5df9fe86862110c1726e0c5b135", + "version" : "3.71.0" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl",