Skip to content

Delete the legacy iOS pixel-firing layer (iOS pixel migration, phase 3) - #6599

Open
federicocappelli wants to merge 8 commits into
fcappelli/pixelkit.migration.callsitesfrom
fcappelli/pixelkit.migration.cleanup
Open

Delete the legacy iOS pixel-firing layer (iOS pixel migration, phase 3)#6599
federicocappelli wants to merge 8 commits into
fcappelli/pixelkit.migration.callsitesfrom
fcappelli/pixelkit.migration.cleanup

Conversation

@federicocappelli

@federicocappelli federicocappelli commented Sep 1, 2026

Copy link
Copy Markdown
Member

Task/Issue URL: https://app.asana.com/1/137249556945/project/1205842942115003/task/1217858720586294

Stacked on #6550 (the call-site migration). Base branch is fcappelli/pixelkit.migration.callsites, not main, so this diff is scoped to this phase only.

Description

Deletes the legacy iOS pixel-firing layer now that every call site fires through PixelKit. Gone: Pixel, DailyPixel, UniquePixel, PersistentPixel, PersistentPixelStoring, their PixelFiring/PixelFiringAsync/DailyPixelFiring protocols, and PixelFiringMock plus the other legacy test mocks. Pixel.Event and PixelParameters survive trimmed into a 12-line Pixel.swift

Important:

  • The VPN's PersistentPixelFiring seams (NetworkProtectionTunnelController, NetworkProtectionPacketTunnelProvider) convert to PixelKit.fire(..., options: .withRetry). PixelKit's retry queue drains automatically after any successful fire, so the explicit sendQueuedPixels() calls in ReportingService and the tunnel provider are deleted.
  • PersistentPixel stamped originalPixelTimestamp on every send, including the first attempt; PixelKit's retry queue only adds it on replay. The initial send of the 16 converted VPN pixels no longer carries that parameter. Inherited behavior difference from the retry-queue design in PixelKit adapter for Pixel.Event (iOS pixel migration, phase 1) #6540, surfaced here because these are the first call sites to actually opt into .withRetry.

Testing Steps

  1. Green CI.

Impact and Risks

What could go wrong?

  • .legacyInitial drops the _u/_unique suffix requirement UniquePixel.fire enforced. Checked every onboarding "unique" case (28 total, including the three call sites that pass a variable rather than a literal) against its wire name - all end in _unique, so nothing that was previously silently dropped starts firing.
  • Those same 16 pixels opt into .withRetry without either retry parameter (originalPixelTimestamp, retriedPixel) declared in an iOS pixel definition - carried over from PersistentPixel, which had the same gap.

Rollback: revert the PR. Nothing downstream depends on this phase.

Internal references: DoD | Eng Expectations | Tech Design Template

Note

Medium Risk
Large deletion in core telemetry and VPN paths; VPN pixels now use PixelKit retry (no originalPixelTimestamp on first send) and onboarding unique firing semantics changed to .legacyInitial.

Overview
Removes the deprecated iOS pixel-firing stack (Pixel, DailyPixel, UniquePixel, PersistentPixel, storage, and legacy PixelFiring protocols/mocks) now that production code fires through PixelKit. Pixel.swift is reduced to Pixel.Event, Pixel.BuildTarget, and PixelParameters; firing stays on PixelEvent+PixelKit.swift.

VPN and app lifecycle no longer depend on PersistentPixelFiring: NetworkProtectionTunnelController injects (any PixelKitFiring)? and fires controller start pixels with options: .withRetry. ReportingService drops sendQueuedPixels()—PixelKit’s retry queue drains after successful fires. AppDependencyProvider removes the persistentPixel dependency.

Onboarding telemetry drops the OnboardingPixelFiring shim; OnboardingPixelReporter uses PixelKitFiring with .legacyInitial for once-ever onboarding events (replacing UniquePixel without requiring _u/_unique suffixes on names). Docs in .cursor/rules/pixels.mdc and PixelKit comments are updated to describe PixelKit as the sole API; the local PixelKitFiring typealias file is removed in favor of PixelKit’s export.

Tests for DailyPixel, PersistentPixel, and related mocks are deleted with the implementations.

Reviewed by Cursor Bugbot for commit 32880bf. Bugbot is set up for automated code reviews on this repo. Configure here.

federicocappelli and others added 2 commits September 1, 2026 11:53
Pixel, DailyPixel, UniquePixel, PersistentPixel, PersistentPixelStoring,
and their PixelFiring/PixelFiringAsync/DailyPixelFiring protocols are
gone, along with PixelFiringMock and the other legacy test mocks.
Pixel.Event and PixelParameters survive - the catalogue of already-
defined iOS pixel names and parameter keys every migrated pixel still
reads from - trimmed into Pixel.swift alongside Pixel.BuildTarget,
which is a load-bearing associated-value type on two existing
Pixel.Event cases.

Four production consumers hadn't moved off the legacy API yet, so this
finishes that first:
- OnboardingPixelReporter + its 1578-line test file: dual Pixel.self/
  UniquePixel.self metatypes collapse into a single
  (any PixelKitFiring)?. Onboarding's "unique" pixels end in `_unique`,
  not `_u`, so `.legacyInitial` is the correct frequency -
  `.uniqueByName` asserts on `_u` and would drop them.
- The VPN's PersistentPixelFiring seams (NetworkProtectionTunnelController,
  NetworkProtectionPacketTunnelProvider) convert to
  `PixelKit.fire(..., options: .withRetry)`. PixelKit's retry queue
  drains automatically after any successful fire, so the explicit
  sendQueuedPixels() calls in ReportingService and the tunnel provider
  are deleted rather than replaced.
- WaitlistExtensions.swift picked up a PixelKit call, then was reverted:
  the surrounding package builds in no target, so migrating it was
  wasted work. Its removal is a separate PR (#6593).

PixelTests.swift split three ways: legacy-mechanism tests deleted with
the mechanism, two pixel-name spot-checks with no PixelKit dependency
moved into PixelEventPixelKitTests.swift, and PixelKitLegacyNamingParityTests
(the naming-contract suite pinning PixelKit's default naming to what
legacy Pixel produced) kept as-is.

.cursor/rules/pixels.mdc rewritten to describe PixelKit as the sole
pixel API, PixelEvent.swift as closed to new cases, and the
(any PixelKitFiring)?/PixelKitMock idiom as the pattern to follow.

Verified: app build, test-target build, and the full local suite -
7180 XCTest + 144 swift-testing, 0 failures (down from 7220+143
pre-cleanup, accounted for by the deleted legacy test files).

Known open items from review, not yet addressed:
- originalPixelTimestamp is no longer added to the initial (non-retry)
  send of the 16 converted VPN pixels - PixelRetryQueue only adds it on
  replay, where legacy PersistentPixel added it on every send.
- Those same 16 pixels are opted into options: .withRetry although
  neither retry parameter is declared in any iOS pixel definition
  (inherited from PersistentPixel, not newly introduced, but now
  explicit via the opt-in).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lsites' into fcappelli/pixelkit.migration.cleanup
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

Legacy iOS pixel system is deprecated – use PixelKit instead.
(Pixel, DailyPixel, UniquePixel, TimedPixel, and PersistentPixel are deprecated).
See https://app.asana.com/1/137249556945/project/1208546505108826/task/1216768405353137?focus=true

Found these new uses:

  • Pixel in iOS/DuckDuckGo/NetworkProtectionTunnelController.swift: pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartAttempt, options: .withRetry)
  • Pixel in iOS/DuckDuckGo/NetworkProtectionTunnelController.swift: pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartSuccess, options: .withRetry)
  • Pixel in iOS/DuckDuckGo/NetworkProtectionTunnelController.swift: pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartFailure.withError(error), options: .withRetry)
  • Pixel in iOS/DuckDuckGo/OnboardingFlow/Pixels/OnboardingPixelReporter.swift: private func fire(event: Pixel.Event, unique: Bool, additionalParameters: [String: String] = [:]) {
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionRekeyAttempt, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionRekeyFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionRekeyCompleted, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelStartAttempt, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelStartFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelStartSuccess, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateAttempt, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateSuccess, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttempt, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttemptFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttemptSuccess, frequency: .legacyDailyAndCount, options: .withRetry)
  • Pixel in iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift: PixelKit.fire(Pixel.Event.networkProtectionConnectionFailureLoopDetected.withError(error), frequency: .legacyDailyAndCount, options: .withRetry)

Generated by 🚫 dangerJS against 32880bf

federicocappelli and others added 4 commits September 1, 2026 15:02
Left over from the adapter-branch fix forward-merged in the previous
commit; this branch already renamed the function and removed DailyPixel/
UniquePixel entirely in Phase 3.

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

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5f0be47. Configure here.

private func fire(event: Pixel.Event, unique: Bool, additionalParameters: [String: String] = [:]) {
// `.legacyInitial`: fires once ever, like legacy UniquePixel.fire, but without UniquePixel's
// `_u`/`_unique` name-suffix requirement (every onboarding "unique" case ends in `Unique`,
// whose name ends in `_unique` - `.uniqueByName` would assert-fail and drop these).

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.

Migration comments over-explain intent

Low Severity

New comments narrate this migration (UniquePixel vs .legacyInitial, sendQueuedPixels vs PixelKit's drain, why Pixel remains) rather than a lasting invariant. Situational notes like these are usually dropped in review.


Please tell me if this was useful or not with a 👍 or 👎.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f0be47. Configure here.

@federicocappelli federicocappelli changed the title Phase 3: delete the legacy iOS pixel-firing layer Delete the legacy iOS pixel-firing layer (Phase 3) Sep 2, 2026
@federicocappelli federicocappelli changed the title Delete the legacy iOS pixel-firing layer (Phase 3) Delete the legacy iOS pixel-firing layer (iOS pixel migration, phase 3) Sep 2, 2026
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.

1 participant