Skip to content

chore!: replace vendored Reachability with NWPathMonitor - #909

Merged
turnipdabeets merged 6 commits into
chore/v4-remove-deprecatedfrom
chore/v4-nwpathmonitor
Oct 7, 2026
Merged

turnipdabeets merged 6 commits into
chore/v4-remove-deprecatedfrom
chore/v4-nwpathmonitor

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Part of the 4.0 stack, on top of #873. It doesn't depend on the macOS/tvOS/watchOS bump in #906. This replaces the vendored Reachability (Ashley Mills, about 400 lines built on SCNetworkReachability) with a small wrapper around NWPathMonitor. Apple deprecated the SCNetworkReachability APIs in the iOS 17.4 / macOS 14.4 SDKs. NWPathMonitor is available on every platform we support.

The wrapper keeps the same internal interface (connection, onReachable/onUnreachable, startNotifier/stopNotifier), so the queues and PostHogContext barely change.

Behavior

Before After
dataMode = .any pauses when unreachable pauses when path.status == .unsatisfied
dataMode = .wifi sends on any reachable path that isn't cellular (Wi-Fi, ethernet) same: !path.usesInterfaceType(.cellular)
VPN on demand / connection on traffic reachable reachable (.requiresConnection)
Path needs a connection with no on-demand flag (e.g. VPN waiting for a password) unavailable reachable (.requiresConnection)
App's cellular data turned off in Settings reachable, cellular (requests fail, retry backoff) unavailable (.unsatisfied, reason .cellularDenied), queues pause
$network_wifi / $network_cellular from the reachability flags from the path, same meaning
watchOS not monitored not monitored (unchanged)
  • First path: NWPathMonitor delivers its first path asynchronously. The old API read flags synchronously, so reads of connection wait for that first path, up to 100 ms after init in total; locally it arrives in about 1 ms. If no path has arrived, connection is nil. In that state .any doesn't pause, .wifi stays paused until a path arrives, and events leave out $network_* rather than reporting false.
  • startNotifier() no longer throws. NWPathMonitor has no failure path, so the queue's "notifier failed to start" fallback is gone.
  • Notifications still go to the main queue by default. They fire on every path update, as the old callback fired on every flags change. NWPathMonitor also reports changes that keep the same connection (e.g. DNS), which now trigger a flush that does nothing. The first startNotifier() reports the current connection, as the old initial check did, so every queue flushes leftover events at launch. Each notification reads the connection when it's delivered, as the old code did.
  • Edge differences: the two rows marked in the table. NWPath doesn't separate a connect-on-demand path from one that needs user action, so both count as reachable; when a request then fails, the normal retry backoff applies. With cellular data turned off for the app, the queues now pause instead of sending requests that fail.

Public API: none. #874 already made ReachabilityError internal. The only public symbol removed is the ReachabilityChangedNotification global, which was already @available(*, unavailable) and isn't in the API snapshot.

Objective-C: none of this was ever exposed to Objective-C. Network autolinks like SystemConfiguration did, so there are no podspec or SPM linker changes.

💚 How did you test it?

  • New Reachability notifier tests cover the start/update/stop rules: the first start reports the current connection once, paths after start are reported, paths after stop aren't, and with no path the wait is bounded. A notification queued before a newer path reports the connection at delivery, so a stale "unavailable" can't pause the queues while online.
  • make test (macOS): all 169 XCTest tests pass. Swift Testing ran 964 tests; 13 issues in PostHogStorage app-group tests fail the same way on the base branch (macOS blocks access to the app-group container on my machine) and are unrelated.
  • A side-by-side harness ran the old SCNetworkReachability code and the new code in one process on macOS (Wi-Fi). Both reported WiFi with no mismatches, and both fired one initial onReachable. I didn't test Wi-Fi/cellular switching or going offline.
  • xcodebuild build succeeds for iOS, tvOS, watchOS, visionOS and Mac Catalyst. make buildSdk wasn't run as-is because xcpretty isn't installed locally.

📝 Checklist

  • 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

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Made with Claude Code, from an audit for leftover code in the 4.0 release work. A separate agent reviewed the PR; its findings are fixed here or listed above.
  • I mapped .wifi to "not cellular" to keep today's behavior. I rejected !path.isExpensive, which would also pause on Wi-Fi tethered to an iPhone hotspot. That's a real behavior change and would need its own changelog entry.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 6, 2026
@turnipdabeets
turnipdabeets marked this pull request as ready for review October 6, 2026 13:26
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 6, 2026 13:26
@turnipdabeets
turnipdabeets force-pushed the chore/v4-nwpathmonitor branch from 114c733 to 1449415 Compare October 6, 2026 13:35
@turnipdabeets
turnipdabeets changed the base branch from chore/v4-raise-apple-platform-minimums to chore/v4-remove-deprecated October 6, 2026 13:35
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-07 20:51:22 UTC
Duration: 230207ms

⚠️ Some Tests Failed

44/45 tests passed, 1 failed


Capture Tests

⚠️ 28/29 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 3018ms
Format Validation.Event Has Uuid ✅ 3282ms
Format Validation.Event Has Lib Properties ✅ 2784ms
Format Validation.Distinct Id Is String ✅ 3078ms
Format Validation.Token Is Present ✅ 659ms
Format Validation.Custom Properties Preserved ✅ 449ms
Format Validation.Event Has Timestamp ✅ 2829ms
Retry Behavior.Retries On 503 ✅ 11899ms
Retry Behavior.Does Not Retry On 400 ✅ 4977ms
Retry Behavior.Does Not Retry On 401 ✅ 2758ms
Retry Behavior.Respects Retry After Header ✅ 7887ms
Retry Behavior.Implements Backoff ✅ 21322ms
Retry Behavior.Retries On 500 ✅ 5640ms
Retry Behavior.Retries On 502 ✅ 9357ms
Retry Behavior.Retries On 504 ✅ 9377ms
Retry Behavior.Max Retries Respected ❌ 22055ms
Deduplication.Generates Unique Uuids ✅ 3261ms
Deduplication.Preserves Uuid On Retry ✅ 9220ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16317ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7849ms
Deduplication.No Duplicate Events In Batch ✅ 2904ms
Deduplication.Different Events Have Different Uuids ✅ 3097ms
Compression.Sends Gzip When Enabled ✅ 2902ms
Batch Format.Uses Proper Batch Structure ✅ 479ms
Batch Format.Flush With No Events Sends Nothing ✅ 291ms
Batch Format.Multiple Events Batched Together ✅ 3100ms
Error Handling.Does Not Retry On 403 ✅ 4911ms
Error Handling.Does Not Retry On 413 ✅ 4883ms
Error Handling.Retries On 408 ✅ 9212ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 6

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 3060ms
Request Payload.Flags Request Uses V2 Query Param ✅ 3028ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2837ms
Request Payload.Flags Request Omits Authorization Header ✅ 3092ms
Request Payload.Token In Flags Body Matches Init ✅ 2974ms
Request Payload.Groups Round Trip ✅ 2938ms
Request Payload.Groups Default To Empty Object ✅ 3088ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 2960ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 3013ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2984ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 3339ms
Request Lifecycle.No Flags Request On Init Alone ✅ 168ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 3005ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5610ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2963ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3141ms

@dustinbyrne dustinbyrne 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.

AI-assisted static review at 144941560cb468c1eb9992a0092b28db8a2245ab. Regression scenarios were not executed.

Comment thread PostHog/Utils/Reachability.swift Outdated
Comment thread .changeset/v4-nwpathmonitor.md Outdated
@dustinbyrne
dustinbyrne requested a review from a team October 6, 2026 17:09
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch from e1cc8cf to 5b6c50e Compare October 7, 2026 01:44
turnipdabeets and others added 4 commits October 6, 2026 21:45
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets force-pushed the chore/v4-nwpathmonitor branch from ec83128 to 8fd604d Compare October 7, 2026 02:05

@dustinbyrne dustinbyrne 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.

Both earlier review comments are addressed. No demonstrated implementation regression found at 8fd604deca89cb595f659badf9a6fceedc06b28b; the inline test-coverage note is nonblocking, and current tests pass.

Reviewed statically with existing CI, without executing new scenarios. Compliance still reports 43/45 despite its green check: the retry-count failure also appears in base-associated CI, and the additional zero-request case has unresolved harness/PR causality.

AI-assisted review.

Comment thread PostHogTests/PostHogContextTest.swift Outdated
@dustinbyrne
dustinbyrne requested a review from a team October 7, 2026 17:27
Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…odes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets merged commit ed0ba3e into chore/v4-remove-deprecated Oct 7, 2026
43 checks passed
@turnipdabeets
turnipdabeets deleted the chore/v4-nwpathmonitor branch October 7, 2026 22:55
turnipdabeets added a commit that referenced this pull request Oct 9, 2026
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match the old reachability notifications

Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: cap the first-path wait and test the reachability notifier

Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: report reachability from the connection at delivery

A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover unknown and known network state in the context test

Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: drive queue reachability tests with update(_:) and cover data modes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Oct 20, 2026
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match the old reachability notifications

Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: cap the first-path wait and test the reachability notifier

Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: report reachability from the connection at delivery

A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover unknown and known network state in the context test

Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: drive queue reachability tests with update(_:) and cover data modes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets turnipdabeets mentioned this pull request Oct 20, 2026
2 of 15 tasks
turnipdabeets added a commit that referenced this pull request Oct 23, 2026
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match the old reachability notifications

Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: cap the first-path wait and test the reachability notifier

Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: report reachability from the connection at delivery

A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover unknown and known network state in the context test

Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: drive queue reachability tests with update(_:) and cover data modes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Oct 26, 2026
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match the old reachability notifications

Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: cap the first-path wait and test the reachability notifier

Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: report reachability from the connection at delivery

A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover unknown and known network state in the context test

Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: drive queue reachability tests with update(_:) and cover data modes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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