Skip to content

chore!: adopt posthog-android 4.0, core 7.0 and posthog-ios 4.0 - #108

Open
turnipdabeets wants to merge 34 commits into
mainfrom
chore/adopt-native-4.0
Open

turnipdabeets wants to merge 34 commits into
mainfrom
chore/adopt-native-4.0

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Important

This is the posthog-kmp 0.11 integration PR. Don't merge it until posthog-android 4.0.0 (+ core 7.0.0) and posthog-ios 4.0.0 are published. Merging releases 0.11.0. CI fails until then, only because the pinned versions don't exist yet (iOS Build: "no versions of 'posthog-ios' match the requirement 4.0.0"). As of 2026-11-13, PostHog/posthog-android#871 and PostHog/posthog-ios#873 are both still open (latest releases: android 3.77.1, iOS 3.97.0). All stacked 0.11 PRs are merged into this branch, and main is merged in up to #123.

💡 Motivation and Context

posthog-kmp 0.11 adopts the native 4.0 majors (PostHog/posthog-android#871, PostHog/posthog-ios#873). KMP has no HTTP code of its own, so Android, JVM, iOS and macOS move to capture v1 with them. JS and Wasm stay on posthog-js.

  • Analytics events go to capture v1 (/i/v1/analytics/events), and the new captureAi sends AI events to /i/v1/ai/events.
  • Per-event options (CaptureOptions.eventOptions, registerOptions(), PostHogEvent.options) and the native property precedence.
  • The feature flag API matches native 4.0: PostHogFeatureFlagsLoaded in the reload callback, onFeatureFlags(), flag properties, evaluationContexts.
  • Session replay always records masked screenshots: SessionRecordingConfig.screenshot is gone, and throttleDelayMs replaces debouncerDelayMs.

The upgrade guide is MIGRATION.md (0.10 → 0.11). From #123, the release also publishes it as the generated migration page.

Docs and onboarding that ship with this release (merge once 0.11.0 is on Maven Central):

What gets released

Artifact Bump Why
com.posthog:posthog-kmp 0.10.0 → 0.11.0 Breaking, released as a minor because the SDK is pre-1.0. Five change intents: adopt-native-4-0, drop-replay-screenshot-option, replay-throttle-delay, feature-flags-native-parity, v1-event-options.

Apps that also pull in posthog-server need 2.25.0 or later, and apps that use posthog-android-surveys-compose declare posthog-android 4.0.0 alongside it. Core 7.0 isn't binary compatible with artifacts built against core 6.x. The changeset and the guide cover both.

Merged into this branch

PR Change
this PR's first commits Pin posthog-android 4.0.0, core 7.0.0, posthog-ios 4.0.0; macOS 11 minimum; throttleDelayMs and projectToken in the native mappings; Apple reports posthog-kmp through PostHogSDK.setSdkName instead of stamping $lib in beforeSend; regenerated sample linkage package
#109 Remove SessionRecordingConfig.screenshot
#113 Add throttleDelayMs, deprecate debouncerDelayMs, apply it on iOS too
481ea99 and follow-ups Match the native 4.0 flag API: PostHogFeatureFlagsLoaded, onFeatureFlags(), set/reset{Person,Group}PropertiesForFlags, evaluationContexts, Apple isFeatureEnabled with defaultValue, empty variant reads as disabled everywhere
#115 PostHog.setAppLanguage(), BCP 47 $locale on JVM
#117 Review follow-ups: web errorsLoading replay, beforeSend person-processing note
#119 Event options, registerOptions()/unregisterOptions() (through the native wrapper-option APIs, PostHog/posthog-android#954 and PostHog/posthog-ios#1045), PostHogEvent.options, captureAi() and its four config options
main Merged up to #123 (Dokka SDK reference), #120, #121, #118

Release review changes in this PR (2026-11-13)

✅ Before release

Release day, after 0.11.0 is on Maven Central

After release

🔍 Follow-ups found while testing (not caused by this PR)

💚 How did you test it?

Built against the native integration branches as of 2026-11-05: posthog-android chore/v4-remove-deprecated @ be3ed4b + #942, published to a local Maven repo as 4.0.0 / core 7.0.0; posthog-ios chore/v4-remove-deprecated @ 3460e53 + #1036 as a local Swift package. The sample was wired to them in a scratch worktree only. Nothing from that wiring is committed.

🤖 Testing locally (native 4.0 isn't published yet)

CI fails here only because posthog-android 4.0.0, core 7.0.0 and posthog-ios 4.0.0 aren't released, so Gradle and SwiftPM can't resolve them. The PR is still testable: point it at the unreleased native branches locally. Don't commit any of this. Verified 2026-11-09: :posthog-kmp:jvmTest, :posthog-kmp:testAndroidHostTest and :posthog-kmp:iosSimulatorArm64Test pass.

Android: publish posthog-android 4.0 (and core 7.0) to mavenLocal:

git clone --depth 1 -b chore/v4-remove-deprecated https://github.com/PostHog/posthog-android.git /tmp/posthog-android
cd /tmp/posthog-android
sed -i.bak 's/^coreVersion=.*/coreVersion=7.0.0/; s/^androidVersion=.*/androidVersion=4.0.0/' gradle.properties
CI=false ./gradlew :posthog:publishToMavenLocal :posthog-android:publishToMavenLocal  # CI=true requires a signing key

Then add mavenLocal() as the first entry of dependencyResolutionManagement { repositories { … } } in settings.gradle.kts. The versions are pinned exactly (4.0.0 / 7.0.0 in gradle/libs.versions.toml), so nothing else changes.

iOS / macOS: in posthog-kmp/build.gradle.kts, point the swiftPackage(…) at the branch:

version = branch("chore/v4-remove-deprecated"),   // was exact(libs.versions.posthog.ios.get())

The build rewrites .swiftpm-locks/; discard those changes too.

Run:

./gradlew :posthog-kmp:jvmTest :posthog-kmp:testAndroidHostTest :posthog-kmp:iosSimulatorArm64Test

Unit tests and builds

Check Result
jvmTest / testAndroidHostTest 70/70, 54/54
iosSimulatorArm64Test / macosArm64Test 43/43 each
jsBrowserTest / jsNodeTest / wasmJsBrowserTest 58/58, 58/58, 44/44
detekt Passes
Samples Android assembleDebug, JVM :sample:run, Wasm distribution, integrateLinkagePackage + Xcode iosApp for simulator and device (both privacy bundles present)

Devices

A scratch scenario in the sample's common code ran the same script on every platform, logged each check, and tagged events with a run ID. Mobile and JVM traffic went through a logging proxy to PostHog US cloud, so the wire format and the stored events were both checked. Each platform ran a main launch and then a restart launch, which force-stops the app first.

Platform Device In-app checks Wire + stored events
Android Pixel 10a (Android 16, physical) 17/17 + 3/3 restart ✅
Android Emulator (Android 17) 17/17 + 3/3 restart ✅
iOS iPhone 14 Pro Max (iOS 26.5.2, physical, direct to cloud) 17/17 + 3/3 restart ✅ stored
iOS iPhone 17 simulator (iOS 26.4) 17/17 + 3/3 restart ✅
JVM macOS desktop, with storage left by 0.10 / core 6 17/17 + 3/3 restart ✅
Wasm Chrome 17/17 (no phx_ step, see follow-ups) ✅ stored

macOS native (macosArm64) is covered by macosArm64Test only; the sample has no native macOS app.

New 4.0 behavior, verified:

  • Events go to POST /i/v1/analytics/events (gzip, Authorization: Bearer phc_…, PostHog-Sdk-Info: posthog-kmp/0.10.0) on Android, JVM and iOS. Session replay uploads go to /s/.
  • $lib = posthog-kmp, $lib_version = 0.10.0 stay in each event body, can be read in beforeSend, and are stored that way. On iOS this comes from setSdkName.
  • A phx_ key disables the SDK: its event never arrived on Android, JVM or iOS.
  • An empty event name is dropped locally.
  • A register("$os_name", …) super property overrides the SDK's value, and unregister restores it.
  • Anonymous events carry the process_person_profile: false capture option instead of a property.
  • $session_id is sent as the top-level session_id.
  • $locale is en-US everywhere. $app_language is en on iOS. On Android it's fr-FR after cmd locale set-app-locales … fr-FR, and absent without a per-app locale or localeConfig, as designed.
  • iOS Application Installed/Opened have $app_version/$app_build and no version/build. Android keeps them.
  • A JSON string flag payload ("plain text") decodes to a String. A non-JSON payload can't be created in PostHog (the API rejects it), so the null path rests on the native tests.

Regression, verified:

  • Capture with string, int, double, both booleans, a list and a nested map arrives with the right JSON types; null values are dropped.
  • beforeSend removes a property, adds one, and drops a named event.
  • CaptureOptions groups and timestamp work, and screen() keeps its name over a $screen_name property.
  • Flags:
    • getFeatureFlag returns the variant.
    • isFeatureEnabled works with and without defaultValue, and a missing flag returns null or the default.
    • getFeatureFlagResult returns JSON payloads as maps, and getAllFeatureFlags works.
    • reloadFeatureFlags fires its callback, and a person-property flag turns on after identify + reload.
  • Identity: identify ($set/$set_once), alias, setPersonProperties, group ($groupidentify), getAnonymousId, getDistinctId and getSessionId all work.
  • captureException arrives with a stack trace.
  • optOut drops events and optIn resumes them.
  • flush works, and super properties persist.
  • After a restart, the distinct ID and flags persist. reset() creates a new ID.
  • Android retried a failed request (PostHog-Attempt: 2) and delivered it.

Re-run on 2026-11-13 (release review)

At 7304360 (after #119 and the main merge), against the current native heads: posthog-android chore/v4-remove-deprecated @ 7bd8b195 published to mavenLocal as 4.0.0 / core 7.0.0, and posthog-ios chore/v4-remove-deprecated @ ef45d7b. Same local wiring as above, not committed.

Check Result
jvmTest / testAndroidHostTest 94/94, 61/61
iosSimulatorArm64Test / macosArm64Test 53/53 each
jsNodeTest / wasmJsBrowserTest 73/73, 58/58
detekt Passes
scripts/generate_markdown.py Passes; writes the reference and the migration page

Devices weren't re-run.

📝 Checklist

If releasing new changes

  • Ran pnpm change to generate a change intent file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Claude Opus 5.5) built KMP against the native 4.0 integration branches, fixed the compile and test failures, and ran the device matrix above. It audited both native 4.0 changesets against the KMP API to complete the changesets and write MIGRATION.md, and ran a main baseline to confirm that the iOS Compose replay gap predates this PR. In the 2026-11-13 release review it merged main, mapped the new members for the reference generator, re-checked the changesets and guide against the current native 4.0 PRs, and fixed the posthog.com page.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 28, 2026
@sampo-s-bot

sampo-s-bot Bot commented Oct 28, 2026

Copy link
Copy Markdown

⚠️ No changeset detected

No new .sampo/changesets/*.md files were detected in this PR. To add one:

  • run sampo add
  • follow the prompts to pick the affected packages and describe the change
  • commit the generated file to this pull request, and it will be detected automatically

If this PR isn't supposed to introduce any changes (e.g., documentation updates), you can ignore this message.

posthog-android 4.0 and posthog-ios 4.0 always record masked screenshots,
so the flag no longer selects anything.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 2ab12cc: 1 Must fix, 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread gradle/libs.versions.toml Resolved
turnipdabeets and others added 5 commits November 4, 2026 15:30
Capture v1 strips `$lib`/`$lib_version` from event properties and reads them
from the PostHog-Sdk-Info header, so the `$lib` stamping in beforeSend no
longer reaches PostHog. Set the native SDK name and version before setup
instead (PostHog/posthog-ios#1027), drop the stamping, and only install the
native beforeSend when the app configures callbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-android 4.0 removed the defaultValue parameter from
getFeatureFlag (PostHog/posthog-android#917), so the native call now
receives two arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ran :sample:integrateLinkagePackage. Package.resolved and .swiftpm-locks
still pin 3.64.1: they can only be re-resolved once posthog-ios 4.0.0 is
tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets and others added 3 commits November 5, 2026 16:27
List the native 4.0 changes KMP apps inherit that the changeset missed:
Xcode 26, empty-name and oversized-ID drops, the stricter redirect rule,
iOS/macOS lifecycle events losing version/build, and the new
$app_language. Drop the $lib bullet: posthog-android#942 and
posthog-ios#1036 keep $lib/$lib_version on capture v1 events, so they
behave as in 0.10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match the native SDKs' name for the replay snapshot throttle and apply it
on iOS too. debouncerDelayMs stays as a deprecated alias, including as a
named constructor argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proxies set up from PostHog's proxy guides before 29 October 2026 drop the Authorization header, so capture v1 gets a 401 and the events are dropped. The managed reverse proxy needs no change.
Pass PostHogFeatureFlagsLoaded to the reloadFeatureFlags callback and call
it before setup on every platform. Add onFeatureFlags, person and group
properties for flags, and PostHogConfig.evaluationContexts. Use the iOS 4.0
isFeatureEnabled with defaultValue, and read an empty variant as disabled on
every platform.
@turnipdabeets
turnipdabeets marked this pull request as ready for review November 10, 2026 23:14

@posthog posthog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 4821ed0: 2 Should fix.

Comment thread MIGRATION.md Resolved
Comment thread MIGRATION.md Resolved
Comment thread .changeset/adopt-native-4-0.md Resolved
Comment thread MIGRATION.md Resolved
@eli-r-ph

Copy link
Copy Markdown

Two items in the description are stale: #115 already merged into this branch, and Apple isFeatureEnabled now uses the native defaultValue overload.

turnipdabeets and others added 4 commits November 10, 2026 18:49
posthog-js calls the handlers it snapshotted before an unsubscribe, so a
listener removed by another listener mid-notification still ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g on JS and Wasm

The posthog-js facade enables person processing before it stores the
overrides, and drops them with person_profiles 'never'. Call the feature
flags extension directly, like Android, JVM, iOS and macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation guide

- a beforeSend that sets or removes a registered $process_person_profile
  keeps identify(), alias(), group() and setPersonProperties() working
- a $process_person_profile passed to capture() is new in 0.11 and wins
  over NEVER
- a registered $session_id replaces the SDK's session ID
- PostHog keeps a personless $set event and only skips the person update

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets

Copy link
Copy Markdown
Collaborator Author

@eli-r-ph Right, 0.11 doesn't expose capture options, registerOptions or captureAi yet; it's the native 4.0 bump plus the flag API. Is it OK to ship without them and add them in a follow-up minor, or do they need to be in this release? If they do, I can work on getting them in.

@posthog posthog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 883184d: 1 Must fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

@eli-r-ph

eli-r-ph commented Nov 11, 2026 •

Copy link
Copy Markdown

Hey @turnipdabeets

0.11 doesn't expose capture options, registerOptions or captureAi yet; it's the native 4.0 bump plus the flag API.

🤔 as long as it isn't writing to /i/v1/* endpoints yet than that should be safe. if not, we should chat (could still be safe as additive change after release, but depends...)

@eli-r-ph

Copy link
Copy Markdown

Correction to my earlier note: 0.11 does write to /i/v1/*, because it wraps the native 4.0 SDKs on Android, iOS, macOS and the JVM. That makes 0.11 a capture v1 release, so I think #119 should land in 0.11 too (one binary break instead of two). These need to land with it:

Does 0.11 publish the same day as the native 4.0 SDKs?

…ve-4.0

# Conflicts:
#	gradle/libs.versions.toml
@turnipdabeets

Copy link
Copy Markdown
Collaborator Author

Updated the description: #115 is marked merged, and the flags row says Apple isFeatureEnabled uses the native defaultValue overload.

* feat: add event options, registerOptions and captureAi

Expose the capture v1 options posthog-android 4.0 and posthog-ios 4.0 take:

- PostHogEventOptions (processPersonProfile, cookielessMode,
  disableSkewCorrection, productTourId), passed through
  CaptureOptions.eventOptions and a new eventOptions parameter on screen,
  identify, alias, group, setPersonProperties and captureException
- PostHog.registerOptions/unregisterOptions, backed by the native wrapper
  option registry
- PostHogEvent.options, so beforeSend can read and change them; option
  keys KMP doesn't know are kept
- PostHog.captureAi, returning the event UUID

posthog-js has no options, so JS and Wasm send the known ones as their
legacy $ properties on capture, screen and captureException, drop them
on the identity calls, and make captureAi a no-op that returns null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep native options when beforeSend returns a constructed event

A callback that built a new PostHogEvent got the default empty options,
which cleared the SDK's process_person_profile = false and turned person
processing on for anonymous events. PostHogEvent.options is now nullable,
and null keeps the options of the event the callback was given, as on
posthog-android. Also corrects the registerOptions KDoc: on JS and Wasm,
posthog-js merges registered properties before before_send runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep options an earlier beforeSend callback set when a later one drops them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat!: send event options as a map and add captureAi lane config

Match the capture v1 contract in sdk-specs#146:

- Event options are a Map<String, Any?> sent as given, so keys the SDK
  doesn't know pass through. PostHogEventOptions becomes the four key
  constants. beforeSend reads and changes the native options map.
- PostHogConfig gains captureAiCompression, captureAiMaxQueueSize,
  captureAiTimeout and captureAiMaxEventBytes, forwarded to the native SDKs.
- A throwing beforeSend callback's debug log names the event and the
  exception type.
- The migration guide sets these through options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: cover #119's binary breaks, the AI endpoint and web registerOptions in the 0.11 migration guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: say beforeSend options are null on web and stored numbers are Long on Apple

On iOS and macOS, values that go through native storage, such as
register() properties and registerOptions() options, reach beforeSend
as Long, while Android keeps Int. Verified on the iOS simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: point the process_person_profile migration rows only at event options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: cover beforeSend clearing options with emptyMap and keeping them with null

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: leave unset captureAi settings to the native SDK defaults

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: say a registered process_person_profile skips identity events

Matches posthog-ios#1067 and posthog-android#979: a registered value no
longer reaches identify, alias, group and setPersonProperties events; a
value passed on the call still applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 922d69e: no findings. This pull request is large, so the review ran in 4 parts with less depth than usual.

turnipdabeets and others added 2 commits November 13, 2026 17:33
The reference generator from #123 stops on any PostHog member that
docs/capabilities.json doesn't map, so the release's reference step
would fail on captureAi, onFeatureFlags, registerOptions,
unregisterOptions, setAppLanguage and the four *PropertiesForFlags
functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-ios 4.0 schedules replay captures from the run loop
(posthog-ios#896, shipped in 3.94.0), so iOS now records Compose
Multiplatform screens, which the pinned 3.64.1 never did. Masking still
walks UIKit views, so text fields, text and images that Compose draws
aren't masked. Verified on the iOS simulator: a Compose TextField was
readable in the frames with default masking, and a ph-no-capture tag on
the Compose view masked the whole view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants