Skip to content

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

Open
turnipdabeets wants to merge 4 commits into
chore/v4-remove-deprecatedfrom
chore/v4-review-nits
Open

turnipdabeets wants to merge 4 commits into
chore/v4-remove-deprecatedfrom
chore/v4-review-nits

Conversation

@turnipdabeets

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
PostHogConfig.propertiesSanitizer, PostHogPropertiesSanitizer message: use setBeforeSend(_:)
PostHogConfig.remoteConfig, PostHogSessionReplayConfig.maskPhotoLibraryImages, PostHogSessionReplayConfig.screenshotModeBackgroundCapture message: delete the assignment

The PostHogLogLevel/PostHogLogEntry stubs are behind #if !SWIFT_PACKAGE || SessionReplay, the gate #975 puts on PostHogConsoleLogLevel/PostHogConsoleLogEntry, so they never point at a type the trait leaves out. The replay config stubs are in an extension in PostHogSessionReplayConfig.swift, so #975's file-level gate covers them too. Either PR can merge first.

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.
  • screenshotMode and didReceiveFeatureFlags: other PRs handle these.

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.

💚 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.
  • iOS-only stubs (PostHogLogLevel, PostHogLogEntry, the replay config properties) compile in the iOS xcodebuild build. I didn't compile a usage check for them against iOS; they use the same declaration patterns as above.
  • 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. Each stub sits next to its replacement rather than in a new file, which avoids editing the Xcode project.
  • 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

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-27T17:41:44.618246+00:00
Duration: 234376ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 2997ms
Format Validation.Event Has Uuid ✅ 2843ms
Format Validation.Event Has Lib Properties ✅ 2774ms
Format Validation.Distinct Id Is String ✅ 2706ms
Format Validation.Token Is Present ✅ 2874ms
Format Validation.Custom Properties Preserved ✅ 2896ms
Format Validation.Event Has Timestamp ✅ 2795ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 2865ms
Retry Behavior.Retries On 503 ✅ 11753ms
Retry Behavior.Does Not Retry On 400 ✅ 4841ms
Retry Behavior.Does Not Retry On 401 ✅ 2452ms
Retry Behavior.Respects Retry After Header ✅ 5299ms
Retry Behavior.Implements Backoff ✅ 21810ms
Retry Behavior.Retries On 500 ✅ 9171ms
Retry Behavior.Retries On 502 ✅ 9286ms
Retry Behavior.Retries On 504 ✅ 5401ms
Retry Behavior.Max Retries Respected ❌ 21298ms
Deduplication.Generates Unique Uuids ✅ 3047ms
Deduplication.Preserves Uuid On Retry ✅ 9153ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16711ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7890ms
Deduplication.No Duplicate Events In Batch ✅ 2806ms
Deduplication.Different Events Have Different Uuids ✅ 2743ms
Compression.Sends Gzip When Enabled ✅ 2812ms
Batch Format.Uses Proper Batch Structure ✅ 2796ms
Batch Format.Flush With No Events Sends Nothing ✅ 233ms
Batch Format.Multiple Events Batched Together ✅ 2915ms
Error Handling.Does Not Retry On 403 ✅ 4823ms
Error Handling.Does Not Retry On 413 ✅ 4725ms
Error Handling.Retries On 408 ✅ 8756ms

Failures

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 ✅ 2749ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2867ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2803ms
Request Payload.Flags Request Omits Authorization Header ✅ 2822ms
Request Payload.Token In Flags Body Matches Init ✅ 2805ms
Request Payload.Groups Round Trip ✅ 2704ms
Request Payload.Groups Default To Empty Object ✅ 2771ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2853ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 3369ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2807ms
Request Lifecycle.No Flags Request On Init Alone ✅ 220ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2914ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5701ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2811ms
Retry Behavior.Retries Flags On 502 ✅ 2925ms
Retry Behavior.Retries Flags On 504 ✅ 2759ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3198ms

@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. What I don't like about this is it allows them to call these Apis but they are no ops which I think could confuse agents.

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.

2 participants