Skip to content

chore: 4.0 fix-it stubs for removed APIs and review nits - #977

Merged
turnipdabeets merged 9 commits into
chore/v4-remove-deprecatedfrom
chore/v4-review-nits
Nov 2, 2026
Merged

turnipdabeets merged 9 commits into
chore/v4-remove-deprecatedfrom
chore/v4-review-nits

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Addresses ioannisj's review on #873 (#873). We're not shipping a last 3.x release with deprecations, so 4.0 itself should point apps at the replacement when they hit a removed API. Pairs with #975 (see the trait gate note below).

1. Unavailable stubs for renamed and removed 3.x APIs

Code that still uses an old name now fails with an error that names the replacement, plus an Xcode fix-it for plain renames, instead of "cannot find in scope":

Old symbol Error / fix-it
PostHogLogLevel, PostHogLogEntry (iOS) renamed to PostHogConsoleLogLevel / PostHogConsoleLogEntry
PostHogConfig(apiKey:), PostHogConfig(apiKey:host:), PostHogConfig.apiKey renamed to init(projectToken:), init(projectToken:host:), projectToken
PostHogConfig.evaluationEnvironments renamed to evaluationContexts
PostHogConfig.PostHogDataMode.cellular renamed to .any (a static property, not an enum case, so init(rawValue: 1) still returns nil and switches stay exhaustive)
PostHogSessionReplayConfig.debouncerDelay (iOS) renamed to throttleDelay
PostHogSDK.getFeatureFlagPayload(_:) message: use getFeatureFlagResult(key, sendFeatureFlagEvent: false)?.payload
PostHogSDK.didReceiveFeatureFlags message: use onFeatureFlags(_:)
PostHogSessionReplayConfig.screenshotMode (iOS) message: delete the assignment, replay always records masked screenshots
PostHogConfig.propertiesSanitizer, PostHogPropertiesSanitizer message: use setBeforeSend(_:)
PostHogConfig.remoteConfig, PostHogSessionReplayConfig.maskPhotoLibraryImages, PostHogSessionReplayConfig.screenshotModeBackgroundCapture message: delete the assignment

All the stubs live in PostHog/PostHogRemovedAPIs.swift, which has a note to delete it a few minor versions into 4.x. Nothing can call them, so deleting the file isn't a breaking change. The replay stubs in it are behind #if os(iOS) && (!SWIFT_PACKAGE || SessionReplay), so they never point at a type the trait leaves out.

The stubs aren't @objc, so Objective-C callers see the old selectors go away like before; this only helps Swift. The apiKey:host: stub has no default host, so PostHogConfig(apiKey:) resolves to exactly one stub and nothing overlaps with the projectToken: initializers.

Not stubbed:

  • Accidental public utilities (Gzip, toISO8601String, UIColor(hex:), dateToMillis, ReachabilityError and so on): apps aren't expected to use them.
  • SPI-moved wrapper internals (PostHogSessionManager, PostHogStorageManager, postHogSdkName, ...): a stub would clash with the @_spi declaration of the same name.
  • The reloadFeatureFlags(_:) callback change: Swift already offers a fix-it (insert _ in), and an old-signature overload would make { } closures ambiguous.

Public API snapshot: swift-symbolgraph-extract leaves out unconditionally unavailable declarations, so api/posthog-ios.public-api.txt doesn't change (I ran the snapshot update and none of the stubs appeared). The stubs aren't callable API, so that's accurate.

2. Keep-in-sync comment

Added to the platform check in the stub PostHogErrorTrackingAutoCaptureIntegration. It refers to "the gate at the top of the file" rather than to a line number.

3. Reachability: queues no longer block setup on the first path

PostHogQueue.updatePauseState now reads a new non-blocking Reachability.connectionIfKnown instead of the waiting connection getter.

For an unknown connection I kept the rule #909 set (and PostHogReplayBatchBoundaryTests pins): .any doesn't pause, .wifi stays paused. That's the same thing that happened before once the 100 ms wait ran out. If a .any send goes out while the device is actually offline, it fails, the records stay on disk, and the send is retried. Then the first path's onReachable/onUnreachable notification sets the real state and flushes. That notification always arrives: startNotifier() reports a path that came in before the notifier started, and update(_:) reports one that comes in after. The read happens inside stateLock, so a first-path notification racing with start() can't be overwritten by the stale unknown.

I first tried pausing every queue while the connection is unknown. That broke the existing "Any data mode sends before the first path arrives" test, so I reverted to the existing contract rather than changing it here.

PostHogContext (for $network_wifi/$network_cellular) still uses the waiting getter. Setup captures Application Installed/Updated/Opened synchronously, so without the wait those events would almost always lose the network properties. The wait ends as soon as the first path arrives (usually a few ms), with 100 ms as the cap. So when setup captures a lifecycle event, setup can still wait for the first path through the context, but the queues don't add any wait. The changeset line ("events captured in the first 100 ms after setup may leave out $network_wifi...") still holds, so I didn't edit it.

4. Rename RRWireframe to RRScreenshotFrame

With wireframe capture gone, this internal type only carries a single screenshot frame. It isn't public, and the replay payload's wireframes key doesn't change. Local names like wireframe and createBasicWireframe stay as they are, to limit conflicts with #911 and #999.

💚 How did you test it?

  • Stub errors: a temporary file in the test target used each macOS-visible stub. Each one produced the intended error and message, and valid 4.0 code in the same file compiled: projectToken inits, .any/.wifi, an exhaustive switch on PostHogDataMode, PostHogDataMode(rawValue: 1), getFeatureFlagResult(_:sendFeatureFlagEvent:). I removed the file before committing.
  • Fix-its: swiftc -typecheck -diagnostic-style llvm on a standalone copy of the declaration patterns (typealias, renamed init, renamed property, enum static property) shows the rename fix-it for each one.
  • All stubs on iOS: a temporary file in the PostHog target used all 15 stubs, including the iOS-only ones, and each produced its intended error against an iOS simulator build. Valid 4.0 code in the same file produced no errors. I removed the file before committing.
  • After moving the stubs into one file: make lint, swift build for macOS and for iOS with traits on and off, and the iOS xcodebuild build. I didn't rerun the full validation below locally; CI covers it on the new head.
  • New test: PostHogReachabilityNotifierTests.connectionIfKnownDoesNotWait. The existing PostHogReplayBatchBoundaryTests ("Any data mode sends before the first path arrives", plus the Wi-Fi-mode test that stays paused until a path arrives) and PostHogLogsQueueTests.reachabilityPauseAndResume cover the queue behavior.
  • Validation (local, Xcode 26.4.1):
    • make lint: passes.
    • make test: fails only in tests outside this change. The app-group PostHogStorage tests ("writes to disk in a project token folder under a group container directory", "excludes new and existing project folders from backup") fail the same way on an unmodified base checkout. A scattered set of PostHogSDK/Surveys/AppLifeCycle tests failed with 30 s request timeouts, and the set changed on every run. An unmodified base checkout running the same suites failed a different set of tests the same way, so I put this down to load from other test runs on the same machine. Before the last commit, which only touches iOS-only files that the macOS run compiles out, every reachability, queue and context suite passed.
    • make testNoTraits: same two pre-existing PostHogStorage failures, nothing else.
    • make buildSdk: exits 127 because xcpretty isn't installed. I ran each step of the recipe without it, and all pass: iOS, buildSdkSpm, buildSdkSpmNoTraits (macOS + iOS), macOS, Mac Catalyst, tvOS, watchOS, visionOS.
    • make apiUpdate: same xcpretty problem. I ran the script's steps without it: no snapshot change from this PR. The local toolchain also renders @Sendable into some existing declarations, but that comes from the toolchain, not from this PR, so I didn't commit it.
    • git diff --check: clean.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

No new changeset: this goes into the 4.0 release entry in .changeset/v4-major-release.md, and none of these changes alter what that entry describes.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Claude Opus 5.5) implemented this from a maintainer's brief that covered three of ioannisj's review threads on #873.

  • Stubs: I added them only where an app is likely to use the old symbol and the stub costs nothing. They're all in one file, as ioannisj asked in review, so they're easy to remove later.
  • Reachability: I took the reviewer's suggestion for the queues, kept chore!: replace vendored Reachability with NWPathMonitor #909's rule for an unknown connection (.any sends, .wifi waits), and kept the wait in the event context, because dropping it there would strip network properties from setup lifecycle events.
  • Worktree builds: the example subprojects reference the package as ../../posthog-ios, so xcodebuild runs from an agent worktree need a temporary posthog-ios symlink next to the worktree. xcpretty isn't installed on this machine, so I ran the buildSdk and apiUpdate steps without it.

🤖 Generated with Claude Code

turnipdabeets and others added 4 commits October 27, 2026 12:54
Apps still using a renamed or removed 3.x API now get a compile error
that names the replacement (an Xcode fix-it for plain renames) instead
of "cannot find in scope". Swift-only; not exposed to Objective-C.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queues read the connection without waiting for NWPathMonitor's first
path and let that path's reachability notification set the real state,
instead of waiting up to 100 ms during setup. An unknown connection
keeps the existing behavior: .any doesn't pause, .wifi stays paused.
Event context still waits, so setup events keep $network_wifi and
$network_cellular.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matches the gate #975 puts on PostHogConsoleLogLevel and
PostHogConsoleLogEntry, so the stubs never reference a type the trait
leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 27, 2026 17:28
@turnipdabeets turnipdabeets self-assigned this Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-11-02T19:33:09.519983+00:00
Duration: 228196ms

⚠️ Some Tests Failed

45/47 tests passed, 2 failed


Capture Tests

⚠️ 28/30 tests passed, 2 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ❌ 531ms
Format Validation.Event Has Uuid ✅ 3037ms
Format Validation.Event Has Lib Properties ✅ 282ms
Format Validation.Distinct Id Is String ✅ 2814ms
Format Validation.Token Is Present ✅ 3108ms
Format Validation.Custom Properties Preserved ✅ 312ms
Format Validation.Event Has Timestamp ✅ 357ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 2731ms
Retry Behavior.Retries On 503 ✅ 11778ms
Retry Behavior.Does Not Retry On 400 ✅ 4933ms
Retry Behavior.Does Not Retry On 401 ✅ 4856ms
Retry Behavior.Respects Retry After Header ✅ 9016ms
Retry Behavior.Implements Backoff ✅ 15429ms
Retry Behavior.Retries On 500 ✅ 9615ms
Retry Behavior.Retries On 502 ✅ 9206ms
Retry Behavior.Retries On 504 ✅ 9423ms
Retry Behavior.Max Retries Respected ❌ 21277ms
Deduplication.Generates Unique Uuids ✅ 2988ms
Deduplication.Preserves Uuid On Retry ✅ 9264ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16245ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7757ms
Deduplication.No Duplicate Events In Batch ✅ 2818ms
Deduplication.Different Events Have Different Uuids ✅ 2928ms
Compression.Sends Gzip When Enabled ✅ 2740ms
Batch Format.Uses Proper Batch Structure ✅ 2871ms
Batch Format.Flush With No Events Sends Nothing ✅ 307ms
Batch Format.Multiple Events Batched Together ✅ 2928ms
Error Handling.Does Not Retry On 403 ✅ 4809ms
Error Handling.Does Not Retry On 413 ✅ 4760ms
Error Handling.Retries On 408 ✅ 9208ms

Failures

format_validation.event_has_required_fields

Expected 1 requests, got 0

retry_behavior.max_retries_respected

Expected 4 requests, got 6

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2728ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2661ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2839ms
Request Payload.Flags Request Omits Authorization Header ✅ 2804ms
Request Payload.Token In Flags Body Matches Init ✅ 2788ms
Request Payload.Groups Round Trip ✅ 2802ms
Request Payload.Groups Default To Empty Object ✅ 2903ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2705ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2837ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2820ms
Request Lifecycle.No Flags Request On Init Alone ✅ 99ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2759ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5486ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2796ms
Retry Behavior.Retries Flags On 502 ✅ 2778ms
Retry Behavior.Retries Flags On 504 ✅ 2856ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2985ms

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

@ioannisj do you think we really need this if we have a migration doc? It might be better to have compiler fail on the removed APIs so agents have to switch them. This still leaves a way to use deprecated APIs. I’m inclined to close this.

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

@ioannisj do you think we really need this if we have a migration doc? It might be better to have compiler fail on the removed APIs so agents have to switch them. This still leaves a way to use deprecated APIs. I’m inclined to close this.

@ioannisj ping again :)

@ioannisj ioannisj left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To be honest what I had in mind was deprecation messages in a few 3.x version before rolling out 4.0. I really like this approach though cause nothing old is really callable, and agents (or human) are presented with the fix right in the compile error, rather jumping to MIGRATION.md and guessing.

There are things we can't cover with stubs, so I think both could work well together. We can keep this around for a few 4.x minor versions before removing. One change I'd made, is define this in a dedicated extension file. Removing them would be just removing that one file from the project if we want to remove clutter down the road.

I think we are missing maybe screenshotMode and didReceiveFeatureFlags which PR body mentions. For screenshotMode the message can be similar to delete the assignment, replay always records masked screenshots for example

@turnipdabeets

turnipdabeets commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

To be honest what I had in mind was deprecation messages in a few 3.x version before rolling out 4.0. I really like this approach though cause nothing old is really callable, and agents (or human) are presented with the fix right in the compile error, rather umping to MIGRATION.md and guessing.

There are things we can't cover with stubs, so I think both could work well together. We can keep this around for a few 4.x minor versions before removing. One change I'd made, is define this in a dedicated extension file. Removing them would be just removing that one file from the project if we want to remove clutter down the road.

I think we are missing maybe screenshotMode and didReceiveFeatureFlags which PR body mentions. For screenshotMode the message can be similar to delete the assignment, replay always records masked screenshots for example

@ioannisj My thought was that this is a major release so it's ok. We can deprecate in 3.x but some people might not even see that between the update to 4.0 and as long as we have a good migration plan agents should be ok. Some of these things were only really ment to be internal anyway. I thought this could maybe confuse the agent to call APIs, but it looks like it shuld get a compiler error so should be ok. I’ll merge.

turnipdabeets and others added 5 commits October 30, 2026 22:03
Puts every unavailable stub in PostHogRemovedAPIs.swift, with a note to
delete the file a few minor versions into 4.x. Adds stubs for
screenshotMode and didReceiveFeatureFlags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With wireframe capture gone, the type only carries a single screenshot
frame. Internal only: the replay payload's `wireframes` key is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets
turnipdabeets merged commit c3f0b71 into chore/v4-remove-deprecated Nov 2, 2026
44 of 46 checks passed
@turnipdabeets
turnipdabeets deleted the chore/v4-review-nits branch November 2, 2026 19:34
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.

2 participants