Flutter
Contents
This is an optional library you can install if you're working with Flutter. It 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.
PostHog supports the iOS, macOS, Android, and Web platforms.
Install with the wizard
Run this command in your project directory to add PostHog to your Flutter project. The wizard detects your framework, installs the SDK, and sets up your first events.
Or set it up manually by following the rest of this guide.
Installation
PostHog is available for install via Pub.
Configuration
Set your PostHog project token and enable automatic event tracking if you want the library to capture lifecycle events for you.
Remember that the application lifecycle events won't have any special context set for you by the time it is initialized. If you are using a self-hosted instance of PostHog you will need to have the public hostname or IP for your instance as well.
To start, add posthog_flutter to your pubspec.yaml:
Then complete the setup for each platform:
For Session Replay and Surveys, you must set up the SDK manually by disabling the
com.posthog.posthog.AUTO_INITmode.
Android setup
There are 2 ways of initializing the SDK, automatically and manually.
Automatically:
Add your PostHog configuration to your AndroidManifest.xml file located in the android/app/src/main:
Or manually (more control and more configurations available):
Add your PostHog configuration to your AndroidManifest.xml file located in the android/app/src/main:
In both cases, you'll also need to update the minimum Android SDK version to 23 in android/app/build.gradle:
iOS setup
There are 2 ways of initializing the SDK, automatically and manually.
The SDK supports both CocoaPods and Swift Package Manager (SPM). Flutter 3.44 and later enable SPM by default. On earlier versions, or if you disabled SPM, enable it with flutter config --enable-swift-package-manager.
Automatically:
Add your PostHog configuration to the Info.plist file located in the ios/Runner directory:
Or manually (more control and more configurations available):
Add your PostHog configuration to the Info.plist file located in the ios/Runner directory:
In both cases, you'll need to set the minimum platform version to iOS 15.0. Building for iOS requires Xcode 26 or later.
For CocoaPods projects, set it in your Podfile:
For Swift Package Manager projects without a Podfile, set the Minimum Deployments version to iOS 15.0 for the Runner target in Xcode (Runner > General > Minimum Deployments). After you change Minimum Deployments, regenerate the iOS project's configuration files:
Dart setup (For manual step only)
If you followed the automatic SDK setup, then there's no more configuration needed in Dart.
If you followed the manual SDK setup:
Web setup
If your project has a web/ directory, this step is required. Posthog().setup() is a no-op on web, so a web build without the snippet below captures nothing.
Add your Web snippet (which you can find in your project settings) in the <header> of your web/index.html file. Write your project token into the snippet as a literal string. It's public, the same token ships to every visitor, and it needs no build-time or deploy-time injection:
For more information please check: /docs/libraries/js
Reproducible Android builds and Shorebird
The Flutter SDK uses the native PostHog Android SDK, which depends on the PostHog core library. The Flutter package permits a range of Android SDK versions, so Gradle can select a newer native version even when your posthog_flutter version hasn't changed. Your pubspec.lock doesn't lock these Gradle dependencies.
This matters for Shorebird patches, which can't update native Android code. To keep PostHog's native dependencies consistent between a release and its patches, pin both native libraries before creating the release:
Inspect the versions selected for your release build. From your app's
androiddirectory, run:TerminalIf you use product flavors, replace
releaseRuntimeClasspathwith your release variant's configuration, such asproductionReleaseRuntimeClasspath. Record the selected versions ofcom.posthog:posthog-androidandcom.posthog:posthog; their version numbers differ.Add strict constraints to your app's
android/app/build.gradle.kts. Replace the placeholders with the versions selected together in the previous step:KotlinFor a Groovy
build.gradle, use the same dependency declarations withversion { strictly "<version>" }inside each constraint. A plain version declaration can still be upgraded during Gradle conflict resolution; strict constraints enforce the selected versions.Run the dependency report again, build your app, and verify that events arrive in PostHog. Commit the constraints and
pubspec.lock, and use the same versions for the release and its patches.
For an existing Shorebird release, use the native versions recorded for that release, not versions resolved by a fresh build today. Pinning PostHog alone doesn't freeze other plugins or your build tools. For broader dependency control, see Gradle dependency locking.
Pins also prevent automatic native bug fixes. When upgrading PostHog, review the pins, select compatible native versions together, and test event capture before shipping a new app-store release. Don't upgrade native dependencies in a Dart-only patch or force an old version below the Flutter package's required minimum.
Capturing events
You can send custom events using capture:
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, orinvite sent.
Setting event properties
Optionally, you can include additional information with the event by including a properties object:
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, once you add the
PosthogObserver - $exception – when the app throws exceptions.
- $autocapture – when the user interacts with elements on Flutter Web, captured by posthog-js (see Interaction autocapture)
- $rageclick – when the user rapidly taps in the same area (iOS/macCatalyst by default, Flutter Web via posthog-js)
Capturing screen views
Screen views aren't captured automatically. Add the PosthogObserver to your app yourself. Without it, your app sends no $screen events at all.
This works with any routing package, not just the plain Navigator API. Add the observer wherever your router takes navigator observers, as shown below for MaterialApp and go_router.
Note: Screen names come from each route's
RouteSettings.name. Most routing packages set this for you. If yours doesn't, name your routes so$screenevents are readable.
Using navigatorObservers
Add the PosthogObserver to record screen views automatically:
Name your routes:
Using go_router
Add the PosthogObserver to record screen views automatically:
Name your routes:
Interaction autocapture
The Flutter SDK doesn't autocapture taps or other widget interactions on iOS or Android. On Flutter Web, interaction autocapture and rage clicks come from posthog-js, which you configure in the posthog.init call in your web/index.html. The Flutter SDK doesn't call posthog.init itself, so these options don't go in PostHogConfig.
Rage click autocapture
Note: On iOS and macCatalyst, the native SDK captures
$rageclickby default throughPostHogConfig.rageClickConfig. These events don't name the widget. Android doesn't support rage clicks. See the iOS rage click configuration for the thresholds.
On Flutter Web, a $rageclick event names the widget a user tapped, but only after you enable autocapture in posthog-js and semantics in Flutter. Without both, you either get no rage clicks or rage clicks that only report flutter-view.
Enable autocapture and rage clicks in
web/index.html. posthog-js detects rage clicks inside its autocapture click handler, so$rageclickdoesn't fire whileautocaptureis off.JavaScriptIf you want to limit autocapture to tappable widgets, use
css_selector_allowlist: ['[role="button"]']. Don't useelement_allowlist: ['button']– Flutter's semantics nodes areflt-semanticselements, not<button>elements, so that allowlist matches nothing. Rage clicks go through the same allowlist check as$autocapture, so an allowlist that matches no semantics node drops rage clicks too.Enable semantics in
main.dart. Flutter Web draws your app into a single canvas, so by default every click targets theflutter-viewelement and$rageclickhas no$el_text. With semantics on, Flutter places aflt-semanticsnode over each widget and tappable nodes receive the click.$rageclickthen includes the widget label androlein$elements_chain. Which field carries the label depends on how Flutter renders the semantics node: a leaf button puts the label in DOM text, which fills$el_text, while a node with children puts it inaria-label.DartSemantics also changes how Flutter Web delivers taps. On a tappable widget, the engine sends a semantic tap action instead of raw pointer events.
onPressedandonTapkeep working, but code that relies onListener,onTapDown/onTapUp, custom gesture recognizers, or drags that start on a button needs testing. Flutter already turns semantics on when a screen reader is active, so assistive-technology users get this behavior today. A custom Dart rage click detector built on raw pointer events can't coexist with semantics on the same widgets, so choose one approach.Optional: give key widgets a stable identifier. Wrap a widget in
Semantics(identifier: 'cashier_deposit', child: ...)and the identifier appears in$elements_chainasattr__flt-semantics-identifier. Flutter puts the identifier on a parentgroupnode rather than on the button node itself, so filter with Elements chain containscashier_depositinstead of matching the clicked element.
Semantics adds per-frame work on web. If a screen has many widgets, profile it before and after you turn semantics on.
These steps work with Flutter 3.44.8 and posthog-js 1.438.0.
Identifying users
We highly recommend reading our section on Identifying users 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:
- userId: Required. A unique identifier for your user. Typically either their email or database ID.
- userProperties: Optional. A dictionary with key:value pairs to set the person properties
- userPropertiesSetOnce: Optional. Similar to
userProperties. See the difference betweenuserPropertiesanduserPropertiesSetOnce
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 Posthog().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.
We strongly recommend reading our docs on alias to best understand how to correctly use this method.
Anonymous vs identified events
PostHog captures two types of events: anonymous and identified
Identified events enable you to attribute events to specific users, and attach person properties. 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 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 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 Flutter SDK captures anonymous events by default. However, this may change depending on your personProfiles config when initializing PostHog:
personProfiles: PostHogPersonProfiles.identifiedOnly(recommended) (default) - Anonymous events are captured by default. PostHog only captures identified events for users where person profiles have already been created.personProfiles: PostHogPersonProfiles.always- Capture identified events for all events.personProfiles: PostHogPersonProfiles.never- Capture anonymous events for all events.
For example:
How to capture identified events
If you've set the personProfiles config to PostHogPersonProfiles.identifiedOnly (the default option), anonymous events are captured by default. Then, to capture identified events, call any of the following functions:
When you call any of these functions, it creates a person profile 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 PostHogPersonProfiles.always to capture identified events by default.
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 Posthog().register, which takes a key and value, and they persist across sessions.
For example, take a look at the following call:
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 Posthog().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 Posthog().identify. More information on this can be found on the Sending User Information section.
Removing stored super properties
Super properties are persisted across sessions so you have to explicitly remove them if they are no longer relevant. In order to stop sending a super property with events, you can use Posthog().unregister, like so:
This will remove 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 Posthog().reset() which takes care of clearing all stored super properties and more.
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 for Flutter 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.
Feature flags
PostHog's feature flags enable you to safely deploy and roll back new features as well as target specific users and groups with them.
Boolean feature flags
Multivariate feature flags
Ensuring flags are loaded before usage
To use the
onFeatureFlagscallback, you must set up the SDK manually. On Android and iOS, disablecom.posthog.posthog.AUTO_INITfirst.
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 onFeatureFlags callback in your config to be notified when flags are loaded:
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:
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 the Flutter SDK 5.31.0+):
The values are forwarded to the native iOS and Android SDKs:
- Bootstrapped identity applies during setup. On a fresh install, setting it before
setup()means events captured synchronously during initialization (likeApplication 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 viaidentify()when person profiles are enabled. This emits$identifyunless capturing is opted out. A different, already-identified person is left untouched.
- An anonymous bootstrap (
- Bootstrapped flags are served until the first
/flagsresponse, then replaced. A complete/flagsresponse takes over entirely, so bootstrapped-only keys don't persist past it. Only enabled flags are seeded: atrueboolean or a non-empty variant string. Afalseor empty value is dropped, matching posthog-js. Seed payloads with the separatefeatureFlagPayloadsoption. Flag values and payloads must be JSON-serializable, or they're dropped. Bootstrapped flags are cleared onreset().
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.
On Flutter web, bootstrap is not applied, so configure it in your posthog.init({...}) snippet instead. See the SDK bootstrapping guide for the cross-SDK overview.
Setting properties for flag evaluation
If a flag targets person or group properties, you can send those properties inline with the next flag evaluation request instead of waiting for a $set event to be ingested. This avoids the race where a flag returns a stale value right after you set a property.
By default these reload feature flags, and the returned Future completes once the reload finishes, so the next getFeatureFlag reflects the new properties. Pass reloadFeatureFlags: false to set several properties before reloading. Use resetPersonPropertiesForFlags() and resetGroupPropertiesForFlags() to clear them. See property overrides for flag evaluation for details.
Experiments (A/B tests)
Since experiments use feature flags, the code for running an experiment is very similar to the feature flags code. See feature flag code examples for Flutter implementation details.
It's also possible to run experiments without using feature flags.
Error tracking
To set up error tracking in your project, see the error tracking docs.
Logs
To set up logs in your Flutter app, follow the Flutter logs installation guide. The SDK exposes Posthog().logger.{trace,debug,info,warn,error,fatal} (and Posthog().captureLog for full control) for sending structured records to PostHog Logs, with batching, offline persistence, and a rate cap built in.
Session replay
Note: Session replay is supported on Flutter Web, Android, and iOS.
To set up session replay web or mobile session replay in your project, all you need to do is install the Flutter SDK, follow the additional installation instructions, and enable "Record user sessions" in your project settings and enable the sessionReplay option.
If you're using Flutter Web, also enable the Canvas capture in your project settings. This is needed as Flutter renders your app using a browser canvas element.
On Flutter Web, masking (maskAllTexts, maskAllImages, PostHogMaskWidget) applies inside that canvas too — declare session_recording.canvasCapture.maskRegionsFn in the posthog.init call in your web/index.html to enable it (requires PostHog Flutter SDK 5.34.0+ and posthog-js 1.408.0+). See masking on Flutter Web under the Flutter tab.
Surveys
Note: Surveys are supported in Flutter for Web, iOS, and Android platforms.
Surveys launched with popover presentation are automatically shown to users matching the display conditions you set up.
Push notifications
The Flutter SDK can register a device for Workflows push notifications and capture when a user opens one. For setup, including automatic and manual registration, capturing opens, opting out, and identity verification, see Push notifications.
Flush
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 flushInterval (default 30 seconds), after which queued events are sent regardless of how many have been gathered:
You can also manually flush the queue to start sending events immediately instead of waiting for the next batch:
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.
Offline behavior
The PostHog Flutter SDK will continue to capture events when the device is offline for Android and Apple platforms. 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
maxQueueSizein the configuration. - When the queue is full, the oldest event is deleted first.
- The queue is flushed when the app is restarted and the device is online.
Opt out of data capture
You can disable data collection for a user at any time using the optOut() method:
This prevents any future events from being sent. It doesn't remove events already captured for the user. To opt the user back in:
optOut() and optIn() require version 5.51.0 or later. Earlier versions use disable() and enable(), which version 6 removes.
To check if a user is opted out:
Amending or dropping events
Since version 5.13.0, you can provide beforeSend callbacks when initializing the SDK to amend or drop events before they are sent to PostHog.
Redacting information in events
beforeSend gives you one place to edit or redact information before it is sent to PostHog. For example:
Dropping events
Return null from the callback to drop the event:
Filtering autocaptured screens
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.
Limitations
The beforeSend callbacks only apply to events captured via Dart APIs:
Posthog().capture()- custom eventsPosthog().screen()- screen events (event name is$screen)Posthog().captureException()- exception events (event name is$exception)
They do not intercept native-initiated events such as:
- Session replay events (
$snapshot) - Application lifecycle events (
Application Opened, etc.)
On iOS, macOS and Android, only the properties you pass are available in the callback. The native SDK adds system properties (like $device_type and $session_id) and registered super properties later. On Windows and Linux, the callback sees every property and option the SDK adds.
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 during initialization by setting the debug option to true in the PostHogConfig object. A common pattern is to set this to true in development environments only using environment variables.
This will enable verbose logs about the inner workings of the SDK.
You can also enable debug by calling the Posthog().debug() method in your code.
Upgrading from version 5 to version 6
Version 6 moves iOS and macOS to posthog-ios 4.0 and Android to posthog-android 4.0, sends events to PostHog's capture v1 API on every platform except web, raises the minimum iOS and macOS versions, and removes the APIs that version 5 deprecated.
If you send events through a reverse proxy or to a self-hosted PostHog instance, read Events go to capture v1 before you upgrade.
Before you upgrade, update to the latest version 5 release and fix its deprecation warnings. The replacements below work in both versions, with one exception: the parameter of getFeatureFlagResult() that controls the $feature_flag_called event is named sendEvent in version 5 and sendFeatureFlagEvent in version 6. Version 6 also removes a few members that version 5 never marked as deprecated: the sendEvent parameter itself, the toMap() and fromMap() helpers, and some members of the platform interface classes.
Platform requirements
| Platform | Version 5 | Version 6 |
|---|---|---|
| iOS | 13.0 | 15.0 |
| macOS | 10.15 | 11.0 |
| Xcode | – | 26 or later |
| Android | API 23 | API 23 (unchanged) |
For CocoaPods projects, raise the platform version in your Podfile:
If your app also targets macOS, do the same in macos/Podfile:
For Swift Package Manager projects without a Podfile, set Minimum Deployments for the Runner target in Xcode (Runner > General > Minimum Deployments), then regenerate the project's configuration files:
Removed APIs
| Removed in version 6 | Use instead |
|---|---|
PostHogConfig.apiKey | PostHogConfig.projectToken |
com.posthog.posthog.API_KEY in AndroidManifest.xml or Info.plist | com.posthog.posthog.PROJECT_TOKEN |
Posthog().disable() and Posthog().enable() | Posthog().optOut() and Posthog().optIn() |
Posthog().getFeatureFlagPayload(key) | Posthog().getFeatureFlagResult(key, sendFeatureFlagEvent: false), then read payload (sendEvent: false in version 5) |
The sendEvent parameter of getFeatureFlagResult() | sendFeatureFlagEvent |
PostHogConfig.sendFeatureFlagEvents | PostHogConfig.sendFeatureFlagEvent |
PostHogSessionReplayConfig.debouncerDelay | PostHogSessionReplayConfig.throttleDelay |
PostHogDataMode.cellular | PostHogDataMode.any, which cellular always behaved like |
toMap() on the config classes, and PostHogFeatureFlagResult.fromMap() | None. They serialize the SDK's internal format and aren't meant for app code |
For example, to read a flag payload. Pass false to keep the old behavior: getFeatureFlagResult() captures a $feature_flag_called event by default, and getFeatureFlagPayload() didn't.
If you implement or mock PosthogFlutterPlatformInterface, PosthogFlutterIO or PosthogFlutterWeb directly, for example in tests:
- Rename
enable()anddisable()tooptIn()andoptOut(). - Remove
getFeatureFlagPayload(). - Rename the
sendEventparameter ofgetFeatureFlagResult()tosendFeatureFlagEvent.
Calls to Posthog() aren't affected by these.
Events go to capture v1
On iOS, macOS, Android, Windows and Linux, version 6 sends events to capture v1 instead of /batch/. iOS and macOS do this through posthog-ios 4.0, Android through posthog-android 4.0, and Windows and Linux in the SDK's Dart client.
Web is unchanged. Flutter web runs posthog-js, which keeps sending events to the endpoints it uses today.
| Data | Version 5 | Version 6 on iOS, macOS, Android, Windows and Linux |
|---|---|---|
| Events | /batch/ | /i/v1/analytics/events |
AI events (captureAi) | – | /i/v1/ai/events |
Session replay, logs and feature flags keep their endpoints.
PostHog Cloud serves both capture v1 endpoints. If you self-host PostHog, check that your instance serves /i/v1/analytics/events and /i/v1/ai/events before you upgrade.
If you send events through your own reverse proxy, update it before you upgrade:
- Forward
/i/v1/analytics/eventsand/i/v1/ai/eventsto PostHog. - Pass
Authorization(the project token, sent asBearer <token>), everyPostHog-*header,Content-Type,Content-EncodingandUser-Agentthrough unchanged. - Accept request bodies up to 20 MB. AI requests are often larger than 1 MB, the default limit in NGINX.
- If your app also runs on web, keep the routes the proxy already forwards for
posthog-js. One proxy that serves every platform needs both sets of routes.
PostHog's managed reverse proxy already does this.
The SDK doesn't fall back to /batch/. A 404 is retried, so events stay queued until the path is served.
Other changes that come with capture v1, on every platform except web:
- Redirects: the SDK only follows a 307 or 308 redirect to the same origin as
host. Any other redirect drops the batch. If your proxy redirects capture requests to another host, pointhostat the final host. - Event validation: PostHog checks each event on its own and drops only the invalid ones, so the rest of the batch is delivered. It drops an event with an empty name, an empty distinct ID, or a name or distinct ID over 200 UTF-8 bytes. Version 5 shortened a long distinct ID and kept the event. If you identify users with a long ID, such as a JWT, shorten or hash it before you upgrade.
$liband$lib_version: PostHog sets them from the SDK'sPostHog-Sdk-Inforequest header. A value you set in properties or inbeforeSenddoesn't reach PostHog.- Personal API keys: on iOS, macOS and Android, a personal API key (
phx_...) inprojectTokendisables the SDK at setup. Use your project token (phc_...).
Property precedence
On iOS, macOS, Android, Windows and Linux, an event's properties are merged in this order, and later values win:
- properties the SDK sets itself, such as device and app context,
$is_identifiedand$session_id - registered super properties (
register) - the properties you pass to
captureor another event-sending method
In version 5, the SDK's own values won over the properties you registered or passed. In version 6, your value wins for that event. A registered $session_id now overrides the SDK's session ID, which can detach events from their session replay.
beforeSend runs at a different point on each platform:
- Windows and Linux:
beforeSendruns after the merge, so it sees and can change every property and option the SDK adds. - iOS, macOS and Android:
beforeSendruns in Dart before the native SDK adds its properties, so it sees only the properties you passed. See Limitations.
Event options
Capture v1 reads processing settings from an options map sent next to the event's properties. capture, screen, identify, setPersonProperties, alias, group and captureException take a new options argument, and beforeSend can read and change PostHogEvent.options.
| Legacy property | Option |
|---|---|
$process_person_profile | process_person_profile |
$cookieless_mode | cookieless_mode |
$ignore_sent_at | disable_skew_correction |
$product_tour_id | product_tour_id |
- The legacy properties still work. An option you pass, or set in
beforeSend, wins over its legacy property. Anulloption counts as unset. - On iOS, macOS, Android, Windows and Linux, other option keys are sent unchanged.
- On web, the known options are sent as their legacy properties and other keys are dropped.
posthog-jssets$process_person_profileitself, soprocess_person_profilehas no effect on web, and the options ofidentify,setPersonProperties,aliasandgroupare dropped.
AI events
Version 6 adds captureAi, which sends AI events such as $ai_generation to PostHog's AI endpoint instead of the events endpoint. It takes the same arguments as capture and goes through the same beforeSend callbacks.
captureAireturns the event's UUID, ornullwhen the event isn't queued:beforeSenddropped it, it's overcaptureAiMaxEventBytes, or the SDK is disabled or opted out.- PostHog drops an event sent with
captureAiwhose name doesn't start with$ai_.capturenever sends events to the AI endpoint, even ones named$ai_*. - AI events are queued separately from other events.
PostHogConfig.captureAiCompression,captureAiMaxQueueSize,captureAiTimeoutandcaptureAiMaxEventBytesconfigure that queue and its requests. - The SDK sends AI events unchanged. It doesn't redact or truncate prompts, outputs or media, so use
beforeSendto remove anything you don't want to send. - On web,
captureAiis a no-op: it sends nothing and returnsnull.
Retries
On iOS, macOS, Android, Windows and Linux:
- The SDK retries network errors and 404, 408, 429, 500, 502, 503 and 504 responses. Other error statuses drop the events.
- When PostHog asks to retry only some events of a batch, the SDK keeps those events queued and removes the rest.
- A 413 splits the batch into smaller requests.
- After a failed send, the wait before the next one doubles from 1 second up to 30 seconds, with up to 25% random jitter.
- A
Retry-Afterheader pauses sending for at most 30 seconds. - Retries have no limit. Queued events stay until they're delivered or a full queue evicts the oldest.
Behavior changes on iOS and macOS
These changes come from posthog-ios 4.0 and need no code changes, but they can affect your data:
- Malformed feature flag payloads: a payload that is malformed JSON, empty, or only whitespace is now
nullinstead of the raw string. PostHog rejects malformed payloads when you save a flag, so this rarely comes up. - Bootstrapped payloads: string values in
PostHogBootstrapConfig.featureFlagPayloads, such as"123"or"true", now stay strings instead of being decoded to numbers or booleans. This matches Android. - Lifecycle events:
Application Installed,Application UpdatedandApplication Openedno longer include the legacyversionandbuildproperties. Update any insights, cohorts or filters that use them to$app_versionand$app_build. Android still sendsversionandbuildfor now.





