From 2444e27f582d548cbc514691b521a837f086a769 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Tue, 1 Sep 2026 11:53:02 +0100 Subject: [PATCH 1/3] Phase 3: delete the legacy iOS pixel-firing layer 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 --- .cursor/rules/pixels.mdc | 51 +- .../Sources/PixelKit/PixelFiring.swift | 2 +- .../Sources/PixelKit/RetryQueue/README.md | 3 +- iOS/Core/DailyPixel.swift | 167 ------- iOS/Core/DailyPixelFiring.swift | 58 --- iOS/Core/PersistentPixel.swift | 292 ----------- iOS/Core/PersistentPixelStoring.swift | 170 ------- iOS/Core/Pixel.swift | 279 +---------- iOS/Core/PixelFiring.swift | 74 --- iOS/Core/PixelFiringAsync.swift | 43 -- iOS/Core/UniquePixel.swift | 109 ----- iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj | 74 --- .../AppLifecycle/AppDependencyProvider.swift | 3 - .../AppServices/ReportingService.swift | 1 - ...rowserPromptManagerDebugPixelHandler.swift | 2 +- .../NetworkProtectionTunnelController.swift | 27 +- ...nboardingSubscriptionPromotionHelper.swift | 2 +- .../Pixels/OnboardingPixelReporter.swift | 38 +- iOS/DuckDuckGo/PixelKitFiring.swift | 26 - .../VPNSubscriptionPromotionHelper.swift | 2 +- iOS/DuckDuckGoTests/DailyPixelTests.swift | 432 ----------------- .../DefaultPersistentPixelStorageTests.swift | 132 ----- .../OnboardingPixelReporterTests.swift | 454 ++++++++---------- .../PersistentPixelTests.swift | 436 ----------------- .../PixelEventPixelKitTests.swift | 52 ++ iOS/DuckDuckGoTests/PixelTests.swift | 292 +---------- .../UnifiedToggleInputCoordinatorTests.swift | 9 - ...etworkProtectionPacketTunnelProvider.swift | 110 +---- .../Core/Statistics/MockPersistentPixel.swift | 67 --- .../Core/Statistics/MockPixelFiring.swift | 226 --------- .../Onboarding/OnboardingFirePixelMock.swift | 69 --- 31 files changed, 330 insertions(+), 3372 deletions(-) delete mode 100644 iOS/Core/DailyPixel.swift delete mode 100644 iOS/Core/DailyPixelFiring.swift delete mode 100644 iOS/Core/PersistentPixel.swift delete mode 100644 iOS/Core/PersistentPixelStoring.swift delete mode 100644 iOS/Core/PixelFiring.swift delete mode 100644 iOS/Core/PixelFiringAsync.swift delete mode 100644 iOS/Core/UniquePixel.swift delete mode 100644 iOS/DuckDuckGo/PixelKitFiring.swift delete mode 100644 iOS/DuckDuckGoTests/DailyPixelTests.swift delete mode 100644 iOS/DuckDuckGoTests/DefaultPersistentPixelStorageTests.swift delete mode 100644 iOS/DuckDuckGoTests/PersistentPixelTests.swift delete mode 100644 iOS/SharedTestUtils/Mocks/Core/Statistics/MockPersistentPixel.swift delete mode 100644 iOS/SharedTestUtils/Mocks/Core/Statistics/MockPixelFiring.swift delete mode 100644 iOS/SharedTestUtils/Mocks/DuckDuckGo/Tutorials/Onboarding/OnboardingFirePixelMock.swift diff --git a/.cursor/rules/pixels.mdc b/.cursor/rules/pixels.mdc index 2eefa817f02..f9757c5a10e 100644 --- a/.cursor/rules/pixels.mdc +++ b/.cursor/rules/pixels.mdc @@ -17,7 +17,7 @@ Pixels have the following requirements: - Do not use values that are overly precise, e.g. if using an integer value in a parameter, bucket it into ranges rather than including the value verbatim - Never include PII, URLs, or other forms of user-identifiable information in pixel names or parameters -For new production iOS code, use PixelKit instead of the legacy iOS `Pixel`, `DailyPixel`, `UniquePixel`, `TimedPixel`, or `PersistentPixel` APIs. Danger exempts only the legacy infrastructure files named by its check, plus test and mock files; these exemptions do not apply to other production code. +PixelKit is the only pixel-firing API on iOS and macOS. The legacy iOS `Pixel`, `DailyPixel`, `UniquePixel`, `TimedPixel`, and `PersistentPixel` classes, and their `PixelFiring`/`PixelFiringAsync`/`DailyPixelFiring` protocols, have been removed; `PixelFiringMock` and the other legacy test mocks went with them - use `PixelKitMock` instead. `Pixel.Event` and `PixelParameters` survive as the catalogue of already-defined iOS pixel names and parameter keys (see below) - they are the only pieces of the old system still in use. ## Types of Pixels @@ -49,15 +49,17 @@ pixelKit.fire(uniqueEvent, frequency: .uniqueByName) ## Pixel Definition Patterns -### Legacy iOS Pixels +### Existing iOS Pixels (`Pixel.Event`) -Existing legacy iOS pixels are defined as cases on `Pixel.Event` in `iOS/Core/PixelEvent.swift`. Each enum case maps to an HTTP pixel name string via a computed `name` property. This section is reference material for that existing system only. +Every iOS pixel defined before the PixelKit migration is a case on `Pixel.Event` in `iOS/Core/PixelEvent.swift`. Each enum case maps to an HTTP pixel name string via a computed `name` property, and every one of them fires through PixelKit: `PixelKit.fire(Pixel.Event.someCase, ...)`. -#### Existing Legacy Structure +**`iOS/Core/PixelEvent.swift` does not take new pixels.** The file carries a top-of-file notice to that effect, and a Danger check hard-fails a PR that adds a new case (to the enum or to the `name` switch) - see `iOS Pixel Injection Pattern (PixelKit)` in `.cursor/BUGBOT.md`. Modifying or removing an existing case is fine. Define a new iOS pixel as its own `PixelKit.Event`-conforming type in a new file instead, the same way macOS already does (see `macOS Pixels (PixelKit)` below). -1. Enum cases are declared in `iOS/Core/PixelEvent.swift`. +#### Existing Structure + +1. Enum cases are declared in `iOS/Core/PixelEvent.swift` (do not add new ones - see above). 2. The `name` computed property in the same file maps cases to pixel name strings. -3. Existing callers use `Pixel.fire`, `DailyPixel.fireDailyAndCount`, or `UniquePixel.fire`. +3. `PixelEvent+PixelKit.swift` conforms `Pixel.Event` to `PixelKit.Event`, so every case fires through `PixelKit.fire`/`fireAsync`. 4. Matching definitions live in `iOS/PixelDefinitions/pixels/definitions/*.json5` - see the `Pixel Validation` section below for more. #### Enum Case Definition @@ -115,9 +117,9 @@ Examples: | Legacy | `.appLaunch` | `"ml"` | Avoid this style for new pixels | | Legacy | `.privacyDashboardOpened` | `"mp"` | Avoid this style for new pixels | -### Existing Legacy Parameterized iOS Cases +### Existing Parameterized iOS Cases -Some existing legacy enum cases have associated values that are interpolated into the pixel name: +Some existing `Pixel.Event` cases have associated values that are interpolated into the pixel name: ```swift // Enum definition with associated value @@ -198,13 +200,7 @@ pixelKit.fire(event, options: .parameters([Parameter.source: "keyboard_shortcut" ## PixelFiring Protocol -There are two unrelated protocols with this name. Use the PixelKit protocol for new production code. The legacy iOS protocol below documents existing infrastructure and testing support. - -### Legacy iOS (`iOS/Core/Pixel`) - -Existing legacy iOS code uses the deprecated `PixelFiring` protocol in `iOS/Core/PixelFiring.swift` for dependency injection and testing. - -### PixelKit (iOS, macOS, and shared packages) +`PixelFiring` is PixelKit's protocol, used for dependency injection and testing on iOS, macOS, and shared packages alike. A consumer module can reference it as `PixelKitFiring` (a public typealias PixelKit exports for exactly this) to avoid any risk of colliding with a same-named type of its own. ```swift // SharedPackages/PixelKit/Sources/PixelKit/PixelFiring.swift @@ -217,7 +213,21 @@ public protocol PixelFiring { ``` Conformers implement only that requirement. Callers use one of two entry points, never the -requirement directly: +requirement directly. + +A type that injects pixel firing for testability declares the dependency as +`(any PixelKitFiring)? = PixelKit.shared`, not a concrete type: + +```swift +private let pixelFiring: (any PixelKitFiring)? + +init(pixelFiring: (any PixelKitFiring)? = PixelKit.shared) { + self.pixelFiring = pixelFiring +} +``` + +Tests inject `PixelKitMock` (`@_spi(Testing) import PixelKit`) and assert against its +`actualFireCalls`, each entry carrying the fired `pixel`, `frequency`, and `additionalParameters`. ```swift // Fire and forget. Correct for almost every pixel: telemetry should not make the caller wait. @@ -322,8 +332,7 @@ Pixels have some default values, please check the Pixel implementation in the re ## Related Files -- `iOS/Core/Pixel.swift` - iOS pixel firing implementation -- `iOS/Core/DailyPixel.swift` - Daily pixel implementation -- `iOS/Core/UniquePixel.swift` - Unique pixel implementation -- `iOS/Core/PixelEvent.swift` - iOS pixel event definitions -- `SharedPackages/PixelKit/Sources/PixelKit/` - Shared PixelKit implementation +- `iOS/Core/PixelEvent.swift` - existing iOS pixel event definitions (closed to new cases) +- `iOS/Core/PixelEvent+PixelKit.swift` - conforms `Pixel.Event` to `PixelKit.Event` +- `iOS/Core/Pixel.swift` - `PixelParameters` (parameter key catalogue) and `Pixel.BuildTarget` +- `SharedPackages/PixelKit/Sources/PixelKit/` - the PixelKit implementation diff --git a/SharedPackages/PixelKit/Sources/PixelKit/PixelFiring.swift b/SharedPackages/PixelKit/Sources/PixelKit/PixelFiring.swift index 4c5fb1dce82..caaae08731b 100644 --- a/SharedPackages/PixelKit/Sources/PixelKit/PixelFiring.swift +++ b/SharedPackages/PixelKit/Sources/PixelKit/PixelFiring.swift @@ -53,7 +53,7 @@ extension PixelFiring { extension PixelKit: PixelFiring {} /// `PixelFiring` under a name that survives being imported alongside another module's own -/// `PixelFiring` protocol (e.g. iOS's legacy `Core.PixelFiring`) without a collision. +/// same-named protocol without a collision. /// /// Writing `PixelKit.PixelFiring` does not work as a workaround: this module also declares a /// top-level `class PixelKit`, so the module name is shadowed and `PixelKit.PixelFiring` resolves diff --git a/SharedPackages/PixelKit/Sources/PixelKit/RetryQueue/README.md b/SharedPackages/PixelKit/Sources/PixelKit/RetryQueue/README.md index afeb33c121c..f08f21283ce 100644 --- a/SharedPackages/PixelKit/Sources/PixelKit/RetryQueue/README.md +++ b/SharedPackages/PixelKit/Sources/PixelKit/RetryQueue/README.md @@ -66,5 +66,4 @@ parameters. The case disappears as those queues age out. ## Related - `PixelKit.Options.retryOnFailure` — the caller-facing switch. -- `iOS/Core/PersistentPixel.swift` — the older, iOS-only system this is a port of. It is deprecated; - new pixels should use PixelKit. +- iOS's older, `PersistentPixel`-based retry system this was ported from and has since replaced. diff --git a/iOS/Core/DailyPixel.swift b/iOS/Core/DailyPixel.swift deleted file mode 100644 index f32a0ed1fe7..00000000000 --- a/iOS/Core/DailyPixel.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// DailyPixel.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Persistence - -/// A variant of pixel that is fired at most once per day. -/// -/// When fired it checks if the pixel was already fired on a given day: -/// - if not - a request is made as with standard Pixel -/// - if it was - nothing happens -/// -/// The 'fire' method mimics standard Pixel API. -/// The 'onComplete' closure is always called - even when no pixel is fired. -/// In those scenarios a 'DailyPixelError' is returned denoting the reason. -/// -/// *** Deprecated. Use PixelKit (`.daily` frequency) for new pixels. *** -public final class DailyPixel { - - public enum Error: Swift.Error { - - case alreadyFired - - } - - public enum Constant { - - static let dailyPixelStorageIdentifier = "com.duckduckgo.daily.pixel.storage" - public static let dailyPixelSuffixes = (dailySuffix: "_daily", countSuffix: "_count") - public static let legacyDailyPixelSuffixes = (dailySuffix: "_d", countSuffix: "_c") - - // Can be used when an existing pixel is already sent, and only a daily version needs to be added. - // Prevents adding a count suffix to the original pixel name for backward compatibility. - public static let dailyAndStandardSuffixes = (dailySuffix: "_daily", countSuffix: "") - - } - - public static var storage: ThrowingKeyValueStoring = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)! - - /// Sends a given Pixel once per day. - /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. - /// Does not append any suffix unlike the alternative function below - public static func fire(pixel: Pixel.Event, - error: Swift.Error? = nil, - withAdditionalParameters params: [String: String] = [:], - includedParameters: [Pixel.QueryParameters] = [.appVersion], - pixelFiring: PixelFiring.Type = Pixel.self, - dailyPixelStore: ThrowingKeyValueStoring = DailyPixel.storage, - onComplete: @escaping (Swift.Error?) -> Void = { _ in }) { - var key: String = pixel.name - - if let error = error { - var errorParams: [String: String] = [:] - errorParams.appendErrorPixelParams(error: error) - key.append(":\(createSortedStringOfValues(from: errorParams))") - } - - if !hasBeenFiredToday(forKey: key, dailyPixelStore: dailyPixelStore) { - do { - try updatePixelLastFireDate(forKey: key, dailyPixelStore: dailyPixelStore) - pixelFiring.fire(pixel: pixel, - error: error, - includedParameters: includedParameters, - withAdditionalParameters: params, - onComplete: onComplete) - } catch let storageError { - Pixel.fire(pixel: .pixelFireSuppressedStorageError, - error: storageError, - withAdditionalParameters: ["suppressedPixel": pixel.name]) - onComplete(Error.alreadyFired) - } - } else { - onComplete(Error.alreadyFired) - } - } - - /// Sends a given Pixel once per day with a `_d` suffix, in addition to every time it is called with a `_c` suffix. - /// This means a pixel will get sent twice the first time it is called per-day, and subsequent calls that day will only send the `_c` variant. - /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. - public static func fireDailyAndCount(pixel: Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String) = Constant.dailyPixelSuffixes, - error: Swift.Error? = nil, - withAdditionalParameters params: [String: String] = [:], - includedParameters: [Pixel.QueryParameters] = [.appVersion], - pixelFiring: PixelFiring.Type = Pixel.self, - dailyPixelStore: ThrowingKeyValueStoring = DailyPixel.storage, - onDailyComplete: @escaping (Swift.Error?) -> Void = { _ in }, - onCountComplete: @escaping (Swift.Error?) -> Void = { _ in }) { - let key: String = pixel.name - - if !hasBeenFiredToday(forKey: key, dailyPixelStore: dailyPixelStore) { - do { - try updatePixelLastFireDate(forKey: key, dailyPixelStore: dailyPixelStore) - pixelFiring.fire( - pixelNamed: pixel.name + pixelNameSuffixes.dailySuffix, - withAdditionalParameters: params, - includedParameters: includedParameters, - onComplete: onDailyComplete - ) - } catch let storageError { - Pixel.fire(pixel: .pixelFireSuppressedStorageError, - error: storageError, - withAdditionalParameters: ["suppressedPixel": pixel.name]) - onDailyComplete(Error.alreadyFired) - } - } else { - onDailyComplete(Error.alreadyFired) - } - var newParams = params - if let error { - newParams.appendErrorPixelParams(error: error) - } - pixelFiring.fire( - pixelNamed: pixel.name + pixelNameSuffixes.countSuffix, - withAdditionalParameters: newParams, - includedParameters: includedParameters, - onComplete: onCountComplete - ) - } - - private static func updatePixelLastFireDate(forKey key: String, dailyPixelStore: ThrowingKeyValueStoring) throws { - try dailyPixelStore.set(Date(), forKey: key) - } - - private static func hasBeenFiredToday(forKey key: String, dailyPixelStore: ThrowingKeyValueStoring) -> Bool { - do { - if let lastFireDate = try dailyPixelStore.object(forKey: key) as? Date { - return Calendar.current.isDateInToday(lastFireDate) - } - return false - } catch { - return true - } - } - - private static func createSortedStringOfValues(from dict: [String: String], maxLength: Int = 50) -> String { - let sortedKeys = dict.keys.sorted() - - let uniqueString = sortedKeys.compactMap { key -> String? in - guard let value = dict[key] else { return nil } - return value - }.joined(separator: ";") - - if uniqueString.count > maxLength { - return String(uniqueString.prefix(maxLength)) - } - - return uniqueString - } - -} diff --git a/iOS/Core/DailyPixelFiring.swift b/iOS/Core/DailyPixelFiring.swift deleted file mode 100644 index ea6ac4ec507..00000000000 --- a/iOS/Core/DailyPixelFiring.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// DailyPixelFiring.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Persistence - -public protocol DailyPixelFiring { - static func fireDaily(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String]) - - static func fireDailyAndCount(pixel: Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String), - error: Swift.Error?, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - pixelFiring: PixelFiring.Type, - dailyPixelStore: ThrowingKeyValueStoring, - onDailyComplete: @escaping (Swift.Error?) -> Void, - onCountComplete: @escaping (Swift.Error?) -> Void) - - static func fireDailyAndCount(_ pixel: Pixel.Event, - error: Swift.Error?, - withAdditionalParameters params: [String: String]) - - - static func fireDaily(_ pixel: Pixel.Event) -} - -extension DailyPixel: DailyPixelFiring { - public static func fireDaily(_ pixel: Pixel.Event, withAdditionalParameters params: [String: String]) { - fire(pixel: pixel, withAdditionalParameters: params) - } - - public static func fireDaily(_ pixel: Pixel.Event) { - fire(pixel: pixel) - } - - public static func fireDailyAndCount(_ pixel: Pixel.Event, error: Swift.Error?, withAdditionalParameters params: [String: String]) { - fireDailyAndCount(pixel: pixel, error: error, withAdditionalParameters: params) - } - -} diff --git a/iOS/Core/PersistentPixel.swift b/iOS/Core/PersistentPixel.swift deleted file mode 100644 index 7fde2050fb7..00000000000 --- a/iOS/Core/PersistentPixel.swift +++ /dev/null @@ -1,292 +0,0 @@ -// -// PersistentPixel.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import os.log -import Networking -import Persistence - -public protocol PersistentPixelFiring { - func fire(pixel: Pixel.Event, - error: Swift.Error?, - includedParameters: [Pixel.QueryParameters], - withAdditionalParameters params: [String: String], - onComplete: @escaping (Error?) -> Void) - - func fireDailyAndCount(pixel: Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String), - error: Swift.Error?, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - completion: @escaping ((dailyPixelStorageError: Error?, countPixelStorageError: Error?)) -> Void) - - func sendQueuedPixels(completion: @escaping (PersistentPixelStorageError?) -> Void) -} - -/// *** Deprecated. Use PixelKit (which also has retry support) for new pixels. *** -public final class PersistentPixel: PersistentPixelFiring { - - enum Constants { - static let lastProcessingDateKey = "com.duckduckgo.ios.persistent-pixel.last-processing-timestamp" - -#if DEBUG - static let minimumProcessingInterval: TimeInterval = 60 -#else - static let minimumProcessingInterval: TimeInterval = 60 * 60 -#endif - } - - private let pixelFiring: PixelFiring.Type - private let dailyPixelFiring: DailyPixelFiring.Type - private let persistentPixelStorage: PersistentPixelStoring - private let lastProcessingDateStorage: KeyValueStoring - private let calendar: Calendar - private let dateGenerator: () -> Date - private let workQueue = DispatchQueue(label: "Persistent Pixel Retry Queue") - - private let dateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter - }() - - public convenience init() { - self.init(pixelFiring: Pixel.self, - dailyPixelFiring: DailyPixel.self, - persistentPixelStorage: DefaultPersistentPixelStorage(), - lastProcessingDateStorage: UserDefaults.standard) - } - - init(pixelFiring: PixelFiring.Type, - dailyPixelFiring: DailyPixelFiring.Type, - persistentPixelStorage: PersistentPixelStoring, - lastProcessingDateStorage: KeyValueStoring, - calendar: Calendar = .current, - dateGenerator: @escaping () -> Date = { Date() }) { - self.pixelFiring = pixelFiring - self.dailyPixelFiring = dailyPixelFiring - self.persistentPixelStorage = persistentPixelStorage - self.lastProcessingDateStorage = lastProcessingDateStorage - self.calendar = calendar - self.dateGenerator = dateGenerator - } - - // MARK: - Pixel Firing - - public func fire(pixel: Pixel.Event, - error: Swift.Error? = nil, - includedParameters: [Pixel.QueryParameters] = [.appVersion], - withAdditionalParameters additionalParameters: [String: String] = [:], - onComplete: @escaping (Error?) -> Void = { _ in }) { - let fireDate = dateGenerator() - let dateString = dateFormatter.string(from: fireDate) - var additionalParameters = additionalParameters - additionalParameters[PixelParameters.originalPixelTimestamp] = dateString - - Logger.pixels.debug("Firing persistent pixel named \(pixel.name)") - - pixelFiring.fire(pixel: pixel, - error: error, - includedParameters: includedParameters, - withAdditionalParameters: additionalParameters) { pixelFireError in - if pixelFireError != nil { - do { - if let error { - additionalParameters.appendErrorPixelParams(error: error) - } - - try self.persistentPixelStorage.append(pixels: [ - PersistentPixelMetadata(eventName: pixel.name, - additionalParameters: additionalParameters, - includedParameters: includedParameters) - ]) - - onComplete(nil) - } catch { - onComplete(error) - } - } - } - } - - public func fireDailyAndCount(pixel: Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String) = DailyPixel.Constant.dailyPixelSuffixes, - error: Swift.Error? = nil, - withAdditionalParameters additionalParameters: [String: String], - includedParameters: [Pixel.QueryParameters] = [.appVersion], - completion: @escaping ((dailyPixelStorageError: Error?, countPixelStorageError: Error?)) -> Void = { _ in }) { - let dispatchGroup = DispatchGroup() - - dispatchGroup.enter() // onDailyComplete - dispatchGroup.enter() // onCountComplete - - var dailyPixelStorageError: Error? - var countPixelStorageError: Error? - - let fireDate = dateGenerator() - let dateString = dateFormatter.string(from: fireDate) - var additionalParameters = additionalParameters - additionalParameters[PixelParameters.originalPixelTimestamp] = dateString - - Logger.general.debug("Firing persistent daily/count pixel named \(pixel.name)") - - dailyPixelFiring.fireDailyAndCount( - pixel: pixel, - pixelNameSuffixes: pixelNameSuffixes, - error: error, - withAdditionalParameters: additionalParameters, - includedParameters: includedParameters, - pixelFiring: Pixel.self, - dailyPixelStore: DailyPixel.storage, - onDailyComplete: { dailyError in - if let dailyError, (dailyError as? DailyPixel.Error) != .alreadyFired { - do { - if let error { additionalParameters.appendErrorPixelParams(error: error) } - Logger.general.debug("Saving persistent daily pixel named \(pixel.name)") - try self.persistentPixelStorage.append(pixels: [ - PersistentPixelMetadata(eventName: pixel.name + pixelNameSuffixes.dailySuffix, - additionalParameters: additionalParameters, - includedParameters: includedParameters) - ]) - } catch { - dailyPixelStorageError = error - } - } - - dispatchGroup.leave() - }, onCountComplete: { countError in - if countError != nil { - do { - if let error { additionalParameters.appendErrorPixelParams(error: error) } - Logger.general.debug("Saving persistent count pixel named \(pixel.name)") - try self.persistentPixelStorage.append(pixels: [ - PersistentPixelMetadata(eventName: pixel.name + pixelNameSuffixes.countSuffix, - additionalParameters: additionalParameters, - includedParameters: includedParameters) - ]) - } catch { - countPixelStorageError = error - } - } - - dispatchGroup.leave() - } - ) - - dispatchGroup.notify(queue: .global()) { - completion((dailyPixelStorageError: dailyPixelStorageError, countPixelStorageError: countPixelStorageError)) - } - } - - // MARK: - Queue Processing - - public func sendQueuedPixels(completion: @escaping (PersistentPixelStorageError?) -> Void) { - workQueue.async { - if let lastProcessingDate = self.lastProcessingDateStorage.object(forKey: Constants.lastProcessingDateKey) as? Date { - let threshold = self.dateGenerator().addingTimeInterval(-Constants.minimumProcessingInterval) - if threshold <= lastProcessingDate { - completion(nil) - return - } - } - - self.lastProcessingDateStorage.set(self.dateGenerator(), forKey: Constants.lastProcessingDateKey) - - do { - let queuedPixels = try self.persistentPixelStorage.storedPixels() - - if queuedPixels.isEmpty { - completion(nil) - return - } - - Logger.general.debug("Persistent pixel retrying \(queuedPixels.count, privacy: .public) pixels") - - self.fire(queuedPixels: queuedPixels) { pixelIDsToRemove in - Logger.general.debug("Persistent pixel retrying done, \(pixelIDsToRemove.count, privacy: .public) pixels successfully sent") - - do { - try self.persistentPixelStorage.remove(pixelsWithIDs: pixelIDsToRemove) - completion(nil) - } catch { - completion(PersistentPixelStorageError.writeError(error)) - } - } - } catch { - completion(PersistentPixelStorageError.readError(error)) - } - } - } - - // MARK: - Private - - /// Sends queued pixels and calls the completion handler with those that should be removed. - private func fire(queuedPixels: [PersistentPixelMetadata], completion: @escaping (Set) -> Void) { - let dispatchGroup = DispatchGroup() - - let pixelIDsAccessQueue = DispatchQueue(label: "Failed Pixel Retry Attempt Metadata Queue") - var pixelIDsToRemove: Set = [] - let currentDate = dateGenerator() - let date28DaysAgo = calendar.date(byAdding: .day, value: -28, to: currentDate) - - for pixelMetadata in queuedPixels { - if let sendDateString = pixelMetadata.timestamp, let sendDate = dateFormatter.date(from: sendDateString), let date28DaysAgo { - if sendDate < date28DaysAgo { - pixelIDsAccessQueue.sync { - _ = pixelIDsToRemove.insert(pixelMetadata.id) - } - continue - } - } else { - // If we don't have a timestamp for some reason, ignore the retry - retries are only useful if they have a timestamp attached. - // It's not expected that this will ever happen, so an assertion failure is used to report it when debugging. - assertionFailure("Did not find a timestamp for pixel \(pixelMetadata.eventName)") - pixelIDsAccessQueue.sync { - _ = pixelIDsToRemove.insert(pixelMetadata.id) - } - continue - } - - var pixelParameters = pixelMetadata.additionalParameters - pixelParameters[PixelParameters.retriedPixel] = "1" - - dispatchGroup.enter() - - pixelFiring.fire( - pixelNamed: pixelMetadata.eventName, - withAdditionalParameters: pixelParameters, - includedParameters: pixelMetadata.includedParameters, - onComplete: { error in - if error == nil { - pixelIDsAccessQueue.sync { - _ = pixelIDsToRemove.insert(pixelMetadata.id) - } - } - - dispatchGroup.leave() - } - ) - } - - dispatchGroup.notify(queue: .global()) { - completion(pixelIDsToRemove) - } - } - -} diff --git a/iOS/Core/PersistentPixelStoring.swift b/iOS/Core/PersistentPixelStoring.swift deleted file mode 100644 index 9210fb787a0..00000000000 --- a/iOS/Core/PersistentPixelStoring.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// PersistentPixelStoring.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Networking - -public struct PersistentPixelMetadata: Identifiable, Codable, Equatable { - - public let id: UUID - public let eventName: String - public let additionalParameters: [String: String] - public let includedParameters: [Pixel.QueryParameters] - - public init(eventName: String, additionalParameters: [String: String], includedParameters: [Pixel.QueryParameters]) { - self.id = UUID() - self.eventName = eventName - self.additionalParameters = additionalParameters - self.includedParameters = includedParameters - } - - var timestamp: String? { - return additionalParameters[PixelParameters.originalPixelTimestamp] - } -} - -protocol PersistentPixelStoring { - func append(pixels: [PersistentPixelMetadata]) throws - func remove(pixelsWithIDs: Set) throws - func storedPixels() throws -> [PersistentPixelMetadata] -} - -public enum PersistentPixelStorageError: Error { - case readError(Error) - case writeError(Error) - case encodingError(Error) - case decodingError(Error) -} - -final class DefaultPersistentPixelStorage: PersistentPixelStoring { - - enum Constants { - static let queuedPixelsFileName = "queued-pixels.json" - static let pixelCountLimit = 100 - } - - private let fileManager: FileManager - private let fileName: String - private let storageDirectory: URL - private let pixelCountLimit: Int - - private let fileAccessQueue = DispatchQueue(label: "Persistent Pixel File Access Queue", qos: .utility) - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private var fileURL: URL { - return storageDirectory.appendingPathComponent(fileName) - } - - init(fileManager: FileManager = .default, - fileName: String = Constants.queuedPixelsFileName, - storageDirectory: URL? = nil, - pixelCountLimit: Int = Constants.pixelCountLimit) { - self.fileManager = fileManager - self.fileName = fileName - self.pixelCountLimit = pixelCountLimit - - if let storageDirectory = storageDirectory { - self.storageDirectory = storageDirectory - } else if let appSupportDirectory = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { - self.storageDirectory = appSupportDirectory - } else { - fatalError("Unable to locate application support directory") - } - } - - func append(pixels newPixels: [PersistentPixelMetadata]) throws { - try fileAccessQueue.sync { - var pixels = try self.readStoredPixelDataFromFileSystem() - pixels.append(contentsOf: newPixels) - - if pixels.count > pixelCountLimit { - pixels = pixels.suffix(Constants.pixelCountLimit) - } - - try writePixelDataToFileSystem(pixels: pixels) - } - } - - func remove(pixelsWithIDs pixelIDs: Set) throws { - try fileAccessQueue.sync { - var pixels = try self.readStoredPixelDataFromFileSystem() - - pixels.removeAll { pixel in - pixelIDs.contains(pixel.id) - } - - try writePixelDataToFileSystem(pixels: pixels) - } - } - - func storedPixels() throws -> [PersistentPixelMetadata] { - try fileAccessQueue.sync { - return try readStoredPixelDataFromFileSystem() - } - } - - // MARK: - Private - - private var cachedPixelMetadata: [PersistentPixelMetadata]? - - private func readStoredPixelDataFromFileSystem() throws -> [PersistentPixelMetadata] { - dispatchPrecondition(condition: .onQueue(fileAccessQueue)) - - if let cachedPixelMetadata { - return cachedPixelMetadata - } - - guard fileManager.fileExists(atPath: fileURL.path) else { - return [] - } - - do { - let pixelFileData = try Data(contentsOf: fileURL) - - do { - let decodedMetadata = try decoder.decode([PersistentPixelMetadata].self, from: pixelFileData) - self.cachedPixelMetadata = decodedMetadata - return decodedMetadata - } catch { - throw PersistentPixelStorageError.decodingError(error) - } - } catch { - throw PersistentPixelStorageError.readError(error) - } - } - - private func writePixelDataToFileSystem(pixels: [PersistentPixelMetadata]) throws { - dispatchPrecondition(condition: .onQueue(fileAccessQueue)) - - do { - let encodedPixelData = try encoder.encode(pixels) - - do { - try encodedPixelData.write(to: fileURL) - self.cachedPixelMetadata = pixels - } catch { - throw PersistentPixelStorageError.writeError(error) - } - } catch { - throw PersistentPixelStorageError.encodingError(error) - } - } - -} diff --git a/iOS/Core/Pixel.swift b/iOS/Core/Pixel.swift index 6097a8b6c25..d16b0c35f22 100644 --- a/iOS/Core/Pixel.swift +++ b/iOS/Core/Pixel.swift @@ -18,14 +18,18 @@ // import Foundation -import PixelKit -import PrivacyConfig -import Common -import FoundationExtensions -import Networking -import os.log - -/// *** Deprecated. Use PixelKit for new pixels. *** + +/// Namespace kept for `Pixel.Event` (see `PixelEvent.swift`) and `Pixel.BuildTarget`, and for +/// `PixelParameters` below - the legacy pixel name/parameter catalogue every PixelKit-migrated +/// pixel still reads from. The firing implementation itself has moved to PixelKit; see +/// `PixelEvent+PixelKit.swift`. +public enum Pixel { + public enum BuildTarget: String { + case app + case vpn + } +} + public struct PixelParameters { public static let url = "url" static let test = "test" @@ -233,262 +237,3 @@ public struct PixelParameters { public static let suggestionsAreSmart = "isSmart" } -public struct PixelValues { - static let test = "1" -} - -/// *** Deprecated. Use PixelKit for new pixels. *** -public class Pixel { - - private struct Constants { - static let tablet = "tablet" - static let phone = "phone" - } - - public enum BuildTarget: String { - case app - case vpn - } - - public static var isDryRun = PixelKitConfig.isDryRun(isProductionBuild: BuildFlags.isProductionBuild) - - private static var isInternalUser: Bool { - DefaultInternalUserDecider(store: InternalUserStore()).isInternalUser - } - - public static let defaultPixelUserAgent: String = PixelUserAgent.default - - public enum QueryParameters: Codable { - case atb - case appVersion - case isInternalUser - } - - - private enum Constant { - static let pixelStorageIdentifier = "com.duckduckgo.pixel.storage" - } - - public static let storage = UserDefaults(suiteName: Constant.pixelStorageIdentifier)! - - private init() { - } - - public static func fire(pixel: Pixel.Event, - forDeviceType deviceType: UIUserInterfaceIdiom? = UIDevice.current.userInterfaceIdiom, - withAdditionalParameters params: [String: String] = [:], - allowedQueryReservedCharacters: CharacterSet? = nil, - withHeaders headers: APIRequest.Headers = APIRequest.Headers(), - includedParameters: [QueryParameters] = [.appVersion], - onComplete: @escaping (Error?) -> Void = { _ in }, - debounce: Int = 0) { - - let date = Date().addingTimeInterval(-TimeInterval(debounce)) - if !pixel.hasBeenFiredSince(pixelStorage: storage, date: date) { - fire( - pixelNamed: pixel.name, - forDeviceType: deviceType, - withAdditionalParameters: params, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - withHeaders: headers, - includedParameters: includedParameters, - onComplete: onComplete - ) - updatePixelLastFireDate(pixel: pixel) - } else { - onComplete(nil) - } - } - - private static func updatePixelLastFireDate(pixel: Pixel.Event) { - storage.set(Date(), forKey: pixel.name) - } - - public static func fire(pixelNamed pixelName: String, - forDeviceType deviceType: UIUserInterfaceIdiom? = UIDevice.current.userInterfaceIdiom, - withAdditionalParameters params: [String: String] = [:], - allowedQueryReservedCharacters: CharacterSet? = nil, - withHeaders headers: APIRequest.Headers = APIRequest.Headers(userAgent: defaultPixelUserAgent), - includedParameters: [QueryParameters] = [.appVersion], - onComplete: @escaping (Error?) -> Void = { _ in }) { - var newParams = params - - // Only add app version if not already present - if includedParameters.contains(.appVersion) && newParams[PixelParameters.appVersion] == nil { - newParams[PixelParameters.appVersion] = AppVersion.shared.versionNumber - } - - guard !isDryRun else { - Logger.pixels.debug("Pixel fired \(pixelName.replacingOccurrences(of: "_", with: "."), privacy: .public) \(params.count > 0 ? "\(params)" : "", privacy: .public)") - - #if DEBUG - Self.writeValidationPixel(pixelName: pixelName, deviceType: deviceType, parameters: newParams) - #endif - - // simulate server response time for Dry Run mode - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - onComplete(nil) - } - return - } - - if isDebugBuild { - newParams[PixelParameters.test] = PixelValues.test - } - if isInternalUser && includedParameters.contains(.isInternalUser) { - newParams[PixelParameters.isInternalUser] = "true" - } - - let url: URL - if let deviceType = deviceType { - let formFactor = deviceType == .pad ? Constants.tablet : Constants.phone - url = URL.makePixelURL(pixelName: pixelName, - formFactor: formFactor, - includeATB: includedParameters.contains(.atb)) - } else { - url = URL.makePixelURL(pixelName: pixelName, includeATB: includedParameters.contains(.atb) ) - } - - let configuration = APIRequest.Configuration(url: url, - queryParameters: newParams, - allowedQueryReservedCharacters: allowedQueryReservedCharacters, - headers: headers) - let request = APIRequest(configuration: configuration, urlSession: .session(useMainThreadCallbackQueue: true)) - request.fetch { _, error in - Logger.pixels.debug("Pixel fired \(pixelName, privacy: .public) \(params, privacy: .public)") - onComplete(error) - } - } - -} - -extension Pixel { - - public static func fire(pixel: Pixel.Event, - error: Error?, - includedParameters: [QueryParameters] = [.appVersion], - withAdditionalParameters params: [String: String] = [:], - onComplete: @escaping (Error?) -> Void = { _ in }) { - var newParams = params - if let error { - newParams.appendErrorPixelParams(error: error) - } - fire(pixel: pixel, withAdditionalParameters: newParams, includedParameters: includedParameters, onComplete: onComplete) - } -} - -private extension Pixel.Event { - - func hasBeenFiredSince(pixelStorage: UserDefaults, date: Date) -> Bool { - if let lastFireDate = pixelStorage.object(forKey: name) as? Date { - return lastFireDate >= date - } - return false - } -} - -extension Dictionary where Key == String, Value == String { - - mutating func appendErrorPixelParams(error: Error) { - let nsError = error as NSError - - self[PixelParameters.errorCode] = "\(nsError.code)" - self[PixelParameters.errorDomain] = nsError.domain - // WARNING: Avoid adding error.description to prevent leaking personal information. - - let underlyingErrorParameters = underlyingErrorParameters(for: error as NSError) - self.merge(underlyingErrorParameters) { first, _ in first } - } - - private func underlyingErrorParameters(for nsError: NSError, level: Int = 0) -> [String: String] { - if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError { - let levelString = (level == 0 ? "" : String(level + 1)) - let errorCodeParameterName = PixelParameters.underlyingErrorCode + levelString - let errorDomainParameterName = PixelParameters.underlyingErrorDomain + levelString - - let currentUnderlyingErrorParameters = [ - errorCodeParameterName: "\(underlyingError.code)", - errorDomainParameterName: underlyingError.domain - // WARNING: Avoid adding error.description to prevent leaking personal information. - ] - - let additionalParameters = underlyingErrorParameters(for: underlyingError, level: level + 1) - return currentUnderlyingErrorParameters.merging(additionalParameters) { first, _ in first } - } else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber { - return [ - PixelParameters.underlyingErrorCode: "\(sqlErrorCode.intValue)", - PixelParameters.underlyingErrorDomain: "NSSQLiteErrorDomain" - ] - } - - return [:] - } - -} - -// MARK: - Local Pixel Validation - -#if DEBUG -extension Pixel { - - private static let validationLogQueue = DispatchQueue(label: "Debug Pixel Validation") - private static var validationLogCleared = false - - private static var validationLogURL: URL { - let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! - return cacheDir.appendingPathComponent("pixel-validation-log.txt") - } - - private static func pixelURI(name: String, parameters: [String: String]) -> String { - guard !parameters.isEmpty else { - return name - } - - let sortedParams = parameters.sorted { $0.key < $1.key } - let queryString = sortedParams - .map { "\($0.key)=\($0.value)" } - .joined(separator: "&") - return "\(name)?\(queryString)" - } - - /// Writes pixel calls to a file in the Caches directory, so that we can validate the pixels against the JSON definitions before they go to production. - /// To use this, trigger your pixel in the iOS Simulator, and then run `./iOS/scripts/validate_pixels.sh`. - static func writeValidationPixel(pixelName: String, deviceType: UIUserInterfaceIdiom?, parameters: [String: String]) { - let formFactor: String - if let deviceType = deviceType { - formFactor = deviceType == .pad ? Constants.tablet : Constants.phone - } else { - formFactor = Constants.phone - } - let fullPixelName = "\(pixelName)_ios_\(formFactor)" - let pixelURI = pixelURI(name: fullPixelName, parameters: parameters) - - writeToValidationLog("Pixel fired: \(pixelURI)") - } - - private static func writeToValidationLog(_ message: String) { - validationLogQueue.async { - let fileURL = validationLogURL - - // Clear the log file on first write of each session - if !validationLogCleared { - try? FileManager.default.removeItem(at: fileURL) - validationLogCleared = true - } - - let entry = message + "\n" - if let data = entry.data(using: .utf8) { - if FileManager.default.fileExists(atPath: fileURL.path) { - if let handle = try? FileHandle(forWritingTo: fileURL) { - handle.seekToEndOfFile() - handle.write(data) - handle.closeFile() - } - } else { - try? data.write(to: fileURL) - } - } - } - } -} -#endif diff --git a/iOS/Core/PixelFiring.swift b/iOS/Core/PixelFiring.swift deleted file mode 100644 index 0a4ee21af02..00000000000 --- a/iOS/Core/PixelFiring.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// PixelFiring.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Networking -import PixelKit - -/// *** Deprecated. Use PixelKit's `PixelFiring` for new pixels. *** -public protocol PixelFiring { - - static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping (Error?) -> Void) - - static func fire(pixel: Pixel.Event, - error: Error?, - includedParameters: [Pixel.QueryParameters], - withAdditionalParameters params: [String: String], - onComplete: @escaping (Error?) -> Void) - - static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String]) - - static func fire(pixelNamed pixelName: String, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping (Error?) -> Void) -} - -extension Pixel: PixelFiring { - public static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping (Error?) -> Void) { - - Self.fire(pixel: pixel, - withAdditionalParameters: params, - includedParameters: includedParameters, - onComplete: onComplete) - } - - public static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String]) { - Self.fire(pixel: pixel, withAdditionalParameters: params) - } - - public static func fire(pixelNamed pixelName: String, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping (Error?) -> Void) { - Self.fire(pixelNamed: pixelName, - withAdditionalParameters: params, - allowedQueryReservedCharacters: nil, - includedParameters: includedParameters, - onComplete: onComplete) - } -} diff --git a/iOS/Core/PixelFiringAsync.swift b/iOS/Core/PixelFiringAsync.swift deleted file mode 100644 index 72f571c6f56..00000000000 --- a/iOS/Core/PixelFiringAsync.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// PixelFiringAsync.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -public protocol PixelFiringAsync { - static func fire(pixel: Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters]) async throws -} - -extension Pixel: PixelFiringAsync { - public static func fire(pixel: Event, - withAdditionalParameters params: [String: String], - includedParameters: [QueryParameters]) async throws { - - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - Pixel.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters) { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } -} diff --git a/iOS/Core/UniquePixel.swift b/iOS/Core/UniquePixel.swift deleted file mode 100644 index 94e67dddfac..00000000000 --- a/iOS/Core/UniquePixel.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// UniquePixel.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Persistence - -/// A variant of pixel that is fired just once. Ever. -/// -/// The 'fire' method mimics standard Pixel API. -/// The 'onComplete' closure is always called - even when no pixel is fired. -/// In those scenarios a 'UniquePixelError' is returned denoting the reason. -/// -/// *** Deprecated. Use PixelKit (`.uniqueByName` frequency) for new pixels. *** -public final class UniquePixel { - - public enum Error: Swift.Error { - - case alreadyFired - - } - - private enum Constant { - - static let uniquePixelStorageIdentifier = "com.duckduckgo.unique.pixel.storage" - - } - - public static var storage: ThrowingKeyValueStoring = UserDefaults(suiteName: Constant.uniquePixelStorageIdentifier)! - private static let calendar: Calendar = { - var calendar = Calendar.current - calendar.timeZone = TimeZone(secondsFromGMT: 0)! - return calendar - }() - - private static let weeksToCoalesceCohort = 6 - - /// Sends a unique Pixel - /// This requires the pixel name to end with `_u` - public static func fire(pixel: Pixel.Event, - withAdditionalParameters params: [String: String] = [:], - includedParameters: [Pixel.QueryParameters] = [.appVersion], - onComplete: @escaping (Swift.Error?) -> Void = { _ in }) { - guard pixel.name.hasSuffix("_u") || pixel.name.hasSuffix("_unique") else { - assertionFailure("Unique pixel: must end with _u or _unique") - return - } - - if !pixel.hasBeenFiredEver(uniquePixelStorage: storage) { - do { - try storage.set(Date(), forKey: pixel.name) - Pixel.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: onComplete) - } catch let storageError { - Pixel.fire(pixel: .pixelFireSuppressedStorageError, - error: storageError, - withAdditionalParameters: ["suppressedPixel": pixel.name]) - onComplete(Error.alreadyFired) - } - } else { - onComplete(Error.alreadyFired) - } - } - - public static func cohort(from cohortLocalDate: Date?) -> String { - guard let cohortLocalDate, - let baseDate = calendar.date(from: .init(year: 2023, month: 1, day: 1)), - let weeksSinceCohortAssigned = calendar.dateComponents([.weekOfYear], from: cohortLocalDate, to: Date()).weekOfYear, - let assignedCohort = calendar.dateComponents([.weekOfYear], from: baseDate, to: cohortLocalDate).weekOfYear else { - return "" - } - - if weeksSinceCohortAssigned > Self.weeksToCoalesceCohort { - return "" - } else { - return "week-" + String(assignedCohort + 1) - } - } -} - -extension Pixel.Event { - - public func lastFireDate(uniquePixelStorage: ThrowingKeyValueStoring) -> Date? { - try? uniquePixelStorage.object(forKey: name) as? Date - } - - func hasBeenFiredEver(uniquePixelStorage: ThrowingKeyValueStoring) -> Bool { - do { - return try uniquePixelStorage.object(forKey: name) != nil - } catch { - return true - } - } - -} diff --git a/iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj b/iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj index dd5db7873a6..aa107b7f9af 100644 --- a/iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj +++ b/iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj @@ -50,8 +50,6 @@ 1DEAADFB2BA71E9A00E25A97 /* SettingsDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEAADFA2BA71E9A00E25A97 /* SettingsDescriptionView.swift */; }; 1DEAADFF2BA7832F00E25A97 /* EmailProtectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEAADFE2BA7832F00E25A97 /* EmailProtectionView.swift */; }; 1E016AB42949FEB500F21625 /* OmniBarNotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E016AB32949FEB500F21625 /* OmniBarNotificationViewModel.swift */; }; - 1E05D1D629C46EBB00BF9A1F /* DailyPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E05D1D529C46EBB00BF9A1F /* DailyPixel.swift */; }; - 1E05D1DB29C47B3300BF9A1F /* DailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E05D1D929C47B2B00BF9A1F /* DailyPixelTests.swift */; }; 1E162605296840D80004127F /* Triangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E162604296840D80004127F /* Triangle.swift */; }; 1E1626072968413B0004127F /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1626062968413B0004127F /* ViewExtension.swift */; }; 1E162615296D910F0004127F /* cookie-icon-animated-40-dark.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E162614296D910F0004127F /* cookie-icon-animated-40-dark.json */; }; @@ -323,10 +321,6 @@ 4B104AF02F6F65B900843054 /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA9D2A8A659C0050218F /* SyncSettingsAdapter.swift */; }; 4B104AF12F6F65B900843054 /* SyncMetricsEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372A0FEF2B2389590033BF7F /* SyncMetricsEventsHandler.swift */; }; 4B274F602AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */; }; - 4B27FBAE2C924EC6007E21A7 /* PersistentPixelStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B27FBAD2C924EC6007E21A7 /* PersistentPixelStoring.swift */; }; - 4B27FBB12C9252F4007E21A7 /* DefaultPersistentPixelStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B27FBAF2C9251B2007E21A7 /* DefaultPersistentPixelStorageTests.swift */; }; - 4B27FBB32C926E51007E21A7 /* PersistentPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B27FBB22C926E51007E21A7 /* PersistentPixel.swift */; }; - 4B27FBB52C927435007E21A7 /* PersistentPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B27FBB42C927435007E21A7 /* PersistentPixelTests.swift */; }; 4B2855352E3825000085D31D /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = EA39B7E1268A1A35000C62CD /* privacy-reference-tests */; }; 4B2C79612C5B27AC00A240CC /* VPNSnoozeActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD96E082C4DCDD2003BC32C /* VPNSnoozeActivityAttributes.swift */; }; 4B2DF8A42E64BBF800FBF11E /* WideEventService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2DF8A32E64BBF400FBF11E /* WideEventService.swift */; }; @@ -548,8 +542,6 @@ 6F03CAFC2C32C6F6004179A8 /* NewTabPageMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CAFB2C32C6F6004179A8 /* NewTabPageMessagesModel.swift */; }; 6F03CAFE2C32DD08004179A8 /* HomePageMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CAFD2C32DD08004179A8 /* HomePageMessagesConfiguration.swift */; }; 6F03CB022C32ED47004179A8 /* NewTabPageMessagesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB002C32ED42004179A8 /* NewTabPageMessagesModelTests.swift */; }; - 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB062C32F173004179A8 /* PixelFiring.swift */; }; - 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */; }; 6F0FEF6D2C52639E0090CDE4 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */; }; 6F1422822D314A5300B6D3DE /* TabInteractionStateDiskSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1422812D314A5300B6D3DE /* TabInteractionStateDiskSource.swift */; }; 6F1422842D314DD100B6D3DE /* TabInteractionStateDiskSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1422832D314DC900B6D3DE /* TabInteractionStateDiskSourceTests.swift */; }; @@ -589,7 +581,6 @@ 6F76B5D62EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F76B5D52EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift */; }; 6F7BACD42CEE084B00F561D8 /* OmniBarEqualityCheckTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7BACD32CEE084100F561D8 /* OmniBarEqualityCheckTests.swift */; }; 6F7FB8E12C660B3E00867DA7 /* NewTabPageFavoritesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7FB8DF2C660B1A00867DA7 /* NewTabPageFavoritesModelTests.swift */; }; - 6F7FB8E32C660BF300867DA7 /* DailyPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7FB8E22C660BF300867DA7 /* DailyPixelFiring.swift */; }; 6F81F61F2F1FB0790007CADD /* BrowsingMenuHeaderDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F81F61E2F1FB0790007CADD /* BrowsingMenuHeaderDataSourceTests.swift */; }; 6F8348E32D01E401005872E3 /* AlternateAppIcons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F8348E22D01E401005872E3 /* AlternateAppIcons.xcassets */; }; 6F87A7942D96AEA3006BFD71 /* OmniBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F87A7932D96AEA3006BFD71 /* OmniBarViewController.swift */; }; @@ -639,7 +630,6 @@ 78BBBA6B2FE42A6700AE8F41 /* VoiceSearchFeedbackView_PreviewMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BBBA6A2FE42A6700AE8F41 /* VoiceSearchFeedbackView_PreviewMocks.swift */; }; 7A0308260000000000000002 /* TabTerminationTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0308260000000000000001 /* TabTerminationTelemetry.swift */; }; 7A0308260000000000000004 /* TabTerminationTelemetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0308260000000000000003 /* TabTerminationTelemetryTests.swift */; }; - 7A0308260000000000000006 /* PixelKitFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0308260000000000000005 /* PixelKitFiring.swift */; }; 7A0308270000000000000002 /* TabTerminationErrorPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0308270000000000000001 /* TabTerminationErrorPage.swift */; }; 7A0308270000000000000004 /* TabTerminationErrorPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0308270000000000000003 /* TabTerminationErrorPageTests.swift */; }; 7A11B0C0D0E0F00102030405 /* IPadOmnibarToolPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11B0C0D0E0F00102030406 /* IPadOmnibarToolPickerController.swift */; }; @@ -1246,7 +1236,6 @@ 98E563C12DE8B32D00E6E75F /* MockVoiceSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B42DE8B32D00E6E75F /* MockVoiceSearchHelper.swift */; }; 98E563C22DE8B32D00E6E75F /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AD2DE8B32D00E6E75F /* MockTextZoomCoordinator.swift */; }; 98E563C42DE8B32D00E6E75F /* MockWebsiteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B52DE8B32D00E6E75F /* MockWebsiteDataManager.swift */; }; - 98E563C52DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */; }; 98E563C62DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639C2DE8B32D00E6E75F /* MockFaviconStore.swift */; }; 98E563C72DE8B32D00E6E75F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639F2DE8B32D00E6E75F /* MockFireproofing.swift */; }; 98E563C82DE8B32D00E6E75F /* MockOmnibarDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A32DE8B32D00E6E75F /* MockOmnibarDependency.swift */; }; @@ -1257,7 +1246,6 @@ 98E563CF2DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift */; }; 98E563D02DE8B32D00E6E75F /* MockOmniBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A22DE8B32D00E6E75F /* MockOmniBar.swift */; }; 98E563D12DE8B32D00E6E75F /* MockUsageSegmentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B12DE8B32D00E6E75F /* MockUsageSegmentation.swift */; }; - 98E563D22DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */; }; 98E563D32DE8B32D00E6E75F /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B02DE8B32D00E6E75F /* MockURLOpener.swift */; }; 98E563D42DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A72DE8B32D00E6E75F /* MockPrivacyDataReporter.swift */; }; 98E563D52DE8B32D00E6E75F /* OnboardingManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B82DE8B32D00E6E75F /* OnboardingManagerMock.swift */; }; @@ -1265,7 +1253,6 @@ 98E563D92DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563972DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift */; }; 98E563DB2DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563962DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift */; }; 98E563DC2DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639B2DE8B32D00E6E75F /* MockDDGSyncing.swift */; }; - 98E563DD2DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */; }; 98E563DE2DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AA2DE8B32D00E6E75F /* MockTabDelegate.swift */; }; 98E563DF2DE8B32D00E6E75F /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AF2DE8B32D00E6E75F /* MockTutorialSettings.swift */; }; 98E563E02DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B92DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift */; }; @@ -1280,7 +1267,6 @@ 98E563E92DE8B32D00E6E75F /* MockVoiceSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B42DE8B32D00E6E75F /* MockVoiceSearchHelper.swift */; }; 98E563EA2DE8B32D00E6E75F /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AD2DE8B32D00E6E75F /* MockTextZoomCoordinator.swift */; }; 98E563EC2DE8B32D00E6E75F /* MockWebsiteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B52DE8B32D00E6E75F /* MockWebsiteDataManager.swift */; }; - 98E563ED2DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */; }; 98E563EE2DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639C2DE8B32D00E6E75F /* MockFaviconStore.swift */; }; 98E563EF2DE8B32D00E6E75F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639F2DE8B32D00E6E75F /* MockFireproofing.swift */; }; 98E563F02DE8B32D00E6E75F /* MockOmnibarDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A32DE8B32D00E6E75F /* MockOmnibarDependency.swift */; }; @@ -1291,7 +1277,6 @@ 98E563F72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift */; }; 98E563F82DE8B32D00E6E75F /* MockOmniBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A22DE8B32D00E6E75F /* MockOmniBar.swift */; }; 98E563F92DE8B32D00E6E75F /* MockUsageSegmentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B12DE8B32D00E6E75F /* MockUsageSegmentation.swift */; }; - 98E563FA2DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */; }; 98E563FB2DE8B32D00E6E75F /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B02DE8B32D00E6E75F /* MockURLOpener.swift */; }; 98E563FC2DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A72DE8B32D00E6E75F /* MockPrivacyDataReporter.swift */; }; 98E563FD2DE8B32D00E6E75F /* OnboardingManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B82DE8B32D00E6E75F /* OnboardingManagerMock.swift */; }; @@ -1299,7 +1284,6 @@ 98E564012DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563972DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift */; }; 98E564032DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563962DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift */; }; 98E564042DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639B2DE8B32D00E6E75F /* MockDDGSyncing.swift */; }; - 98E564052DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */; }; 98E564062DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AA2DE8B32D00E6E75F /* MockTabDelegate.swift */; }; 98E564072DE8B32D00E6E75F /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AF2DE8B32D00E6E75F /* MockTutorialSettings.swift */; }; 98E564082DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B92DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift */; }; @@ -1314,7 +1298,6 @@ 98E564112DE8B32D00E6E75F /* MockVoiceSearchHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B42DE8B32D00E6E75F /* MockVoiceSearchHelper.swift */; }; 98E564122DE8B32D00E6E75F /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AD2DE8B32D00E6E75F /* MockTextZoomCoordinator.swift */; }; 98E564142DE8B32D00E6E75F /* MockWebsiteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B52DE8B32D00E6E75F /* MockWebsiteDataManager.swift */; }; - 98E564152DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */; }; 98E564162DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639C2DE8B32D00E6E75F /* MockFaviconStore.swift */; }; 98E564172DE8B32D00E6E75F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639F2DE8B32D00E6E75F /* MockFireproofing.swift */; }; 98E564182DE8B32D00E6E75F /* MockOmnibarDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A32DE8B32D00E6E75F /* MockOmnibarDependency.swift */; }; @@ -1325,7 +1308,6 @@ 98E5641F2DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift */; }; 98E564202DE8B32D00E6E75F /* MockOmniBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A22DE8B32D00E6E75F /* MockOmniBar.swift */; }; 98E564212DE8B32D00E6E75F /* MockUsageSegmentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B12DE8B32D00E6E75F /* MockUsageSegmentation.swift */; }; - 98E564222DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */; }; 98E564232DE8B32D00E6E75F /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B02DE8B32D00E6E75F /* MockURLOpener.swift */; }; 98E564242DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A72DE8B32D00E6E75F /* MockPrivacyDataReporter.swift */; }; 98E564252DE8B32D00E6E75F /* OnboardingManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B82DE8B32D00E6E75F /* OnboardingManagerMock.swift */; }; @@ -1333,7 +1315,6 @@ 98E564292DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563972DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift */; }; 98E5642B2DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563962DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift */; }; 98E5642C2DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639B2DE8B32D00E6E75F /* MockDDGSyncing.swift */; }; - 98E5642D2DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */; }; 98E5642E2DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AA2DE8B32D00E6E75F /* MockTabDelegate.swift */; }; 98E5642F2DE8B32D00E6E75F /* MockTutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AF2DE8B32D00E6E75F /* MockTutorialSettings.swift */; }; 98E564302DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B92DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift */; }; @@ -1349,14 +1330,11 @@ 98E5644E2DE8DDDC00E6E75F /* MockBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563982DE8B32D00E6E75F /* MockBundle.swift */; }; 98E5644F2DE8DDDC00E6E75F /* MockOmniBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A22DE8B32D00E6E75F /* MockOmniBar.swift */; }; 98E564502DE8DDDC00E6E75F /* MockMessageNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A12DE8B32D00E6E75F /* MockMessageNavigationDelegate.swift */; }; - 98E564522DE8DDDC00E6E75F /* OnboardingFirePixelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */; }; - 98E564532DE8DDDC00E6E75F /* MockPersistentPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */; }; 98E564552DE8DDDC00E6E75F /* MockFireproofing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639F2DE8B32D00E6E75F /* MockFireproofing.swift */; }; 98E564562DE8DDDC00E6E75F /* MockTabPreviewsSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AC2DE8B32D00E6E75F /* MockTabPreviewsSource.swift */; }; 98E564572DE8DDDC00E6E75F /* PrivacyConfigurationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E564362DE8B6BF00E6E75F /* PrivacyConfigurationManagerMock.swift */; }; 98E564582DE8DDDC00E6E75F /* OnboardingPixelReporterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B92DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift */; }; 98E564592DE8DDDC00E6E75F /* MockDDGSyncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E5639B2DE8B32D00E6E75F /* MockDDGSyncing.swift */; }; - 98E5645A2DE8DDDC00E6E75F /* MockPixelFiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */; }; 98E5645B2DE8DDDC00E6E75F /* MockUsageSegmentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563B12DE8B32D00E6E75F /* MockUsageSegmentation.swift */; }; 98E5645C2DE8DDDC00E6E75F /* MockTextZoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563AD2DE8B32D00E6E75F /* MockTextZoomCoordinator.swift */; }; 98E5645D2DE8DDDC00E6E75F /* MockBookmarksCoreDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E563972DE8B32D00E6E75F /* MockBookmarksCoreDataStorage.swift */; }; @@ -1962,7 +1940,6 @@ BD408CED2DCDB17900265476 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = BD408CEC2DCDB17900265476 /* ZIPFoundation */; }; BD862E092B30F63E0073E2EE /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD862E082B30F63E0073E2EE /* VPNMetadataCollector.swift */; }; BDAA7F492E413DB7003A0833 /* DBPMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDAA7F482E413DB7003A0833 /* DBPMetadataCollector.swift */; }; - BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC234F62B27F51100D3C798 /* UniquePixel.swift */; }; BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; BDE219E62C406D19005D5884 /* SubscriptionDataReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE219E52C406D19005D5884 /* SubscriptionDataReporting.swift */; }; BDE219EA2C457B46005D5884 /* SubscriptionDataReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE219E92C457B46005D5884 /* SubscriptionDataReporterTests.swift */; }; @@ -2992,8 +2969,6 @@ 1DEAADFA2BA71E9A00E25A97 /* SettingsDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDescriptionView.swift; sourceTree = ""; }; 1DEAADFE2BA7832F00E25A97 /* EmailProtectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailProtectionView.swift; sourceTree = ""; }; 1E016AB32949FEB500F21625 /* OmniBarNotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarNotificationViewModel.swift; sourceTree = ""; }; - 1E05D1D529C46EBB00BF9A1F /* DailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyPixel.swift; sourceTree = ""; }; - 1E05D1D929C47B2B00BF9A1F /* DailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyPixelTests.swift; sourceTree = ""; }; 1E162604296840D80004127F /* Triangle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Triangle.swift; sourceTree = ""; }; 1E1626062968413B0004127F /* ViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = ""; }; 1E162614296D910F0004127F /* cookie-icon-animated-40-dark.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "cookie-icon-animated-40-dark.json"; sourceTree = ""; }; @@ -3253,10 +3228,6 @@ 4B1BAFF03022A5BB00314B62 /* OpenAction.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenAction.entitlements; sourceTree = ""; }; 4B1BAFF13022A7E200314B62 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; 4B274F5F2AFEAECC003F0745 /* NetworkProtectionWidgetRefreshModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionWidgetRefreshModel.swift; sourceTree = ""; }; - 4B27FBAD2C924EC6007E21A7 /* PersistentPixelStoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentPixelStoring.swift; sourceTree = ""; }; - 4B27FBAF2C9251B2007E21A7 /* DefaultPersistentPixelStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPersistentPixelStorageTests.swift; sourceTree = ""; }; - 4B27FBB22C926E51007E21A7 /* PersistentPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentPixel.swift; sourceTree = ""; }; - 4B27FBB42C927435007E21A7 /* PersistentPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentPixelTests.swift; sourceTree = ""; }; 4B2DF8A32E64BBF400FBF11E /* WideEventService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventService.swift; sourceTree = ""; }; 4B2DF8A52E64C67000FBF11E /* WideEventServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventServiceTests.swift; sourceTree = ""; }; 4B359FD22FB52B810020D70F /* PrivacyStatsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsDatabase.swift; sourceTree = ""; }; @@ -3432,8 +3403,6 @@ 6F03CAFB2C32C6F6004179A8 /* NewTabPageMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageMessagesModel.swift; sourceTree = ""; }; 6F03CAFD2C32DD08004179A8 /* HomePageMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageMessagesConfiguration.swift; sourceTree = ""; }; 6F03CB002C32ED42004179A8 /* NewTabPageMessagesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageMessagesModelTests.swift; sourceTree = ""; }; - 6F03CB062C32F173004179A8 /* PixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiring.swift; sourceTree = ""; }; - 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelFiringAsync.swift; sourceTree = ""; }; 6F0FEF6C2C52639E0090CDE4 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; 6F1422812D314A5300B6D3DE /* TabInteractionStateDiskSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabInteractionStateDiskSource.swift; sourceTree = ""; }; 6F1422832D314DC900B6D3DE /* TabInteractionStateDiskSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabInteractionStateDiskSourceTests.swift; sourceTree = ""; }; @@ -3473,7 +3442,6 @@ 6F76B5D52EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAutoClearActionDelegate.swift; sourceTree = ""; }; 6F7BACD32CEE084100F561D8 /* OmniBarEqualityCheckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarEqualityCheckTests.swift; sourceTree = ""; }; 6F7FB8DF2C660B1A00867DA7 /* NewTabPageFavoritesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesModelTests.swift; sourceTree = ""; }; - 6F7FB8E22C660BF300867DA7 /* DailyPixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyPixelFiring.swift; sourceTree = ""; }; 6F81F61E2F1FB0790007CADD /* BrowsingMenuHeaderDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuHeaderDataSourceTests.swift; sourceTree = ""; }; 6F8348E22D01E401005872E3 /* AlternateAppIcons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AlternateAppIcons.xcassets; sourceTree = ""; }; 6F87A7932D96AEA3006BFD71 /* OmniBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarViewController.swift; sourceTree = ""; }; @@ -3519,7 +3487,6 @@ 78BBBA6A2FE42A6700AE8F41 /* VoiceSearchFeedbackView_PreviewMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceSearchFeedbackView_PreviewMocks.swift; sourceTree = ""; }; 7A0308260000000000000001 /* TabTerminationTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTerminationTelemetry.swift; sourceTree = ""; }; 7A0308260000000000000003 /* TabTerminationTelemetryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTerminationTelemetryTests.swift; sourceTree = ""; }; - 7A0308260000000000000005 /* PixelKitFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelKitFiring.swift; sourceTree = ""; }; 7A0308270000000000000001 /* TabTerminationErrorPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTerminationErrorPage.swift; sourceTree = ""; }; 7A0308270000000000000003 /* TabTerminationErrorPageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTerminationErrorPageTests.swift; sourceTree = ""; }; 7A11B0C0D0E0F00102030406 /* IPadOmnibarToolPickerController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IPadOmnibarToolPickerController.swift; sourceTree = ""; }; @@ -4350,8 +4317,6 @@ 98E563A12DE8B32D00E6E75F /* MockMessageNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMessageNavigationDelegate.swift; sourceTree = ""; }; 98E563A22DE8B32D00E6E75F /* MockOmniBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOmniBar.swift; sourceTree = ""; }; 98E563A32DE8B32D00E6E75F /* MockOmnibarDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOmnibarDependency.swift; sourceTree = ""; }; - 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPersistentPixel.swift; sourceTree = ""; }; - 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPixelFiring.swift; sourceTree = ""; }; 98E563A62DE8B32D00E6E75F /* MockPrivacyConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyConfiguration.swift; sourceTree = ""; }; 98E563A72DE8B32D00E6E75F /* MockPrivacyDataReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyDataReporter.swift; sourceTree = ""; }; 98E563A82DE8B32D00E6E75F /* MockSecureVault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSecureVault.swift; sourceTree = ""; }; @@ -4367,7 +4332,6 @@ 98E563B32DE8B32D00E6E75F /* MockVariantManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVariantManager.swift; sourceTree = ""; }; 98E563B42DE8B32D00E6E75F /* MockVoiceSearchHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVoiceSearchHelper.swift; sourceTree = ""; }; 98E563B52DE8B32D00E6E75F /* MockWebsiteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebsiteDataManager.swift; sourceTree = ""; }; - 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFirePixelMock.swift; sourceTree = ""; }; 98E563B72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHostingControllerMock.swift; sourceTree = ""; }; 98E563B82DE8B32D00E6E75F /* OnboardingManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManagerMock.swift; sourceTree = ""; }; 98E563B92DE8B32D00E6E75F /* OnboardingPixelReporterMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPixelReporterMock.swift; sourceTree = ""; }; @@ -4814,7 +4778,6 @@ BD7008D32E1F1EED00CF9C3E /* DataBrokerProtection-iOS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = "DataBrokerProtection-iOS.xctestplan"; path = "LocalPackages/DataBrokerProtection-iOS/Tests/DataBrokerProtection-iOS.xctestplan"; sourceTree = ""; }; BD862E082B30F63E0073E2EE /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = ""; }; BDAA7F482E413DB7003A0833 /* DBPMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPMetadataCollector.swift; sourceTree = ""; }; - BDC234F62B27F51100D3C798 /* UniquePixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniquePixel.swift; sourceTree = ""; }; BDE219E52C406D19005D5884 /* SubscriptionDataReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionDataReporting.swift; sourceTree = ""; }; BDE219E92C457B46005D5884 /* SubscriptionDataReporterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionDataReporterTests.swift; sourceTree = ""; }; BDE91CD52C6294020005CB74 /* FeedbackCategoryProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackCategoryProviding.swift; sourceTree = ""; }; @@ -6833,8 +6796,6 @@ 362E3EBE2E7184D800E4C762 /* Statistics */ = { isa = PBXGroup; children = ( - 98E563A52DE8B32D00E6E75F /* MockPixelFiring.swift */, - 98E563A42DE8B32D00E6E75F /* MockPersistentPixel.swift */, ); path = Statistics; sourceTree = ""; @@ -6934,7 +6895,6 @@ 97770B752EC1ED7D00CACA68 /* OnboardingStepsHelper.swift */, 9F3668742EAF0A6900AA8670 /* MockContextualOnboardingStatusProvider.swift */, 98E563932DE8B32D00E6E75F /* ContextualOnboardingPresenterMock.swift */, - 98E563B62DE8B32D00E6E75F /* OnboardingFirePixelMock.swift */, 98E563B72DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift */, 98E563B82DE8B32D00E6E75F /* OnboardingManagerMock.swift */, 9FC0E3D82FAB05B200B11ECD /* MockOnboardingIntroContentProvider.swift */, @@ -11385,16 +11345,9 @@ A0CDC8DDC4354433A6952FF0 /* LegacyPixelStateMigration.swift */, 0C1FD7423FDC4DE994AE2329 /* PixelUserAgent.swift */, 5C522B740A724721A361FC42 /* PixelKitExtensionSetup.swift */, - BDC234F62B27F51100D3C798 /* UniquePixel.swift */, 853A717520F62FE800FE60BC /* Pixel.swift */, FC0A7B0A2F00000100AAB001 /* PixelKitParameterProvider.swift */, - 6F03CB062C32F173004179A8 /* PixelFiring.swift */, - 6F03CB082C32F331004179A8 /* PixelFiringAsync.swift */, - 6F7FB8E22C660BF300867DA7 /* DailyPixelFiring.swift */, - 1E05D1D529C46EBB00BF9A1F /* DailyPixel.swift */, 85E242162AB1B54D000F3E28 /* ReturnUserMeasurement.swift */, - 4B27FBB22C926E51007E21A7 /* PersistentPixel.swift */, - 4B27FBAD2C924EC6007E21A7 /* PersistentPixelStoring.swift */, ); name = Statistics; sourceTree = ""; @@ -11428,7 +11381,6 @@ isa = PBXGroup; children = ( 850250B420D80419002199C7 /* AtbAndVariantCleanupTests.swift */, - 1E05D1D929C47B2B00BF9A1F /* DailyPixelTests.swift */, 851B1281221FE64E004781BC /* ImproveOnboardingExperiment1Tests.swift */, F1134ECB1F40EA0300B73467 /* Parser */, 853A717720F645FB00FE60BC /* PixelTests.swift */, @@ -11437,8 +11389,6 @@ 83EDCC3F1F86B895005CDFCD /* StatisticsLoaderTests.swift */, F1134ED31F40F12B00B73467 /* Store */, 85C11E4020904BBE00BFFEB4 /* VariantManagerTests.swift */, - 4B27FBB42C927435007E21A7 /* PersistentPixelTests.swift */, - 4B27FBAF2C9251B2007E21A7 /* DefaultPersistentPixelStorageTests.swift */, A6E8A70CF1DFF21DC84B150F /* LaunchTimeMetricsProcessorTests.swift */, B8FD41799FD12C35CD0E1600 /* LaunchTimeMetricsSubscriberTests.swift */, ); @@ -11612,7 +11562,6 @@ F1617C141E57336D00DEDCAF /* TabManager.swift */, 7A0308270000000000000001 /* TabTerminationErrorPage.swift */, 7A0308260000000000000001 /* TabTerminationTelemetry.swift */, - 7A0308260000000000000005 /* PixelKitFiring.swift */, F13B4BF51F18C75D00814661 /* Model */, F13B4BF61F18C76600814661 /* UI */, ); @@ -14506,7 +14455,6 @@ 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */, 7A0308270000000000000002 /* TabTerminationErrorPage.swift in Sources */, 7A0308260000000000000002 /* TabTerminationTelemetry.swift in Sources */, - 7A0308260000000000000006 /* PixelKitFiring.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, 859DB8162CE6263C001F7210 /* TextZoomEditorView.swift in Sources */, CBB4A8772FD890E800A65956 /* SuggestionHistoryDeletion.swift in Sources */, @@ -14986,7 +14934,6 @@ D625AAEC2BBEF27600BC189A /* TabURLInterceptorTests.swift in Sources */, 97FCCDD72F60ACA200EDB025 /* MockAIChatContentHandlingDelegate.swift in Sources */, 5694372B2BE3F2D900C0881B /* SyncErrorHandlerTests.swift in Sources */, - 4B27FBB52C927435007E21A7 /* PersistentPixelTests.swift in Sources */, 987130C7294AAB9F00AB05E0 /* MenuBookmarksViewModelTests.swift in Sources */, 9FFDA83D2DFA8F6F007923F7 /* MockAudioSessionManager.swift in Sources */, C1A74BB02EC2618D00870321 /* AutofillServiceTests.swift in Sources */, @@ -15105,7 +15052,6 @@ C1EA266A2EB572E1006A863C /* AutofillExtensionSettingsViewModelTests.swift in Sources */, 5B6D651F2FB4D30A00BDCF4D /* CompleteDownloadRowViewModelTests.swift in Sources */, 5B6D65212FBA000000BDCF4D /* FilePreviewHelperTests.swift in Sources */, - 98E563ED2DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */, 98E563EE2DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */, CB781CCF30112EC9008DCD60 /* UTISessionMonitorTests.swift in Sources */, 98E563EF2DE8B32D00E6E75F /* MockFireproofing.swift in Sources */, @@ -15130,7 +15076,6 @@ BBFF22EF2F3469160055A710 /* MockFreeTrialConversionInstrumentationService.swift in Sources */, 98E563F82DE8B32D00E6E75F /* MockOmniBar.swift in Sources */, 98E563F92DE8B32D00E6E75F /* MockUsageSegmentation.swift in Sources */, - 98E563FA2DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */, 98E563FB2DE8B32D00E6E75F /* MockURLOpener.swift in Sources */, 98E563FC2DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */, 98E563FD2DE8B32D00E6E75F /* OnboardingManagerMock.swift in Sources */, @@ -15145,7 +15090,6 @@ 98E564032DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift in Sources */, 98E564042DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */, CB781CCD3010E961008DCD60 /* AutocompleteSuggestionsPixelsTests.swift in Sources */, - 98E564052DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */, BBC3AF632FF6954500280FC7 /* SettingsDeepLinkSectionTests.swift in Sources */, 98E564062DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */, 98E564072DE8B32D00E6E75F /* MockTutorialSettings.swift in Sources */, @@ -15212,7 +15156,6 @@ F40F843728C939760081AE75 /* AutofillLoginListViewModelTests.swift in Sources */, 986CD5532DE913EE00AFA98A /* MockFavoritesListInteracting.swift in Sources */, 3192F4582E6F1E5500FC2A95 /* LaunchSourceManagerTests.swift in Sources */, - 1E05D1DB29C47B3300BF9A1F /* DailyPixelTests.swift in Sources */, 98E564332DE8B64300E6E75F /* HistoryMocks.swift in Sources */, 56D2D65C2D5F748200C59354 /* NetworkProtectionDNSSettingsViewModelTests.swift in Sources */, 9F60CBA92E9892CF004D2367 /* SurveyLastSearchStateRefresherTests.swift in Sources */, @@ -15264,7 +15207,6 @@ 9F5BEA7E2D4382430045E484 /* MaliciousSiteProtectionDatasetsFetcherTests.swift in Sources */, 9F5BEA7F2D4382430045E484 /* MaliciousSiteProtectionManagerTests.swift in Sources */, 9F3361C12F2B3A0900E0C21A /* MockContextualDaxDialogsFactory.swift in Sources */, - 4B27FBB12C9252F4007E21A7 /* DefaultPersistentPixelStorageTests.swift in Sources */, C14C7EB22EF3B77400ABCDC0 /* AIChatContextualQuickActionTests.swift in Sources */, 564DE45A2C450BE600D23241 /* DaxDialogsNewTabTests.swift in Sources */, 8521FDE6238D414B00A44CC3 /* FileStoreTests.swift in Sources */, @@ -15477,7 +15419,6 @@ 98E564112DE8B32D00E6E75F /* MockVoiceSearchHelper.swift in Sources */, 98E564122DE8B32D00E6E75F /* MockTextZoomCoordinator.swift in Sources */, 98E564142DE8B32D00E6E75F /* MockWebsiteDataManager.swift in Sources */, - 98E564152DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */, 98E564162DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */, 98E564172DE8B32D00E6E75F /* MockFireproofing.swift in Sources */, 98E564182DE8B32D00E6E75F /* MockOmnibarDependency.swift in Sources */, @@ -15495,7 +15436,6 @@ 986CD54C2DE9139400AFA98A /* HomePageMessagesConfigurationMock.swift in Sources */, 80C947FF2F53E819002EDB79 /* Tab+ConvenienceInitializer.swift in Sources */, 9F387E0E2EA9A30B006861F8 /* MockNewAddressBarPickerStorage.swift in Sources */, - 98E564222DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */, 98E564232DE8B32D00E6E75F /* MockURLOpener.swift in Sources */, 98E564242DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */, 98E564252DE8B32D00E6E75F /* OnboardingManagerMock.swift in Sources */, @@ -15517,7 +15457,6 @@ 98E5642B2DE8B32D00E6E75F /* MockAIChatSettingsProvider.swift in Sources */, 98E5642C2DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */, 98E564AC2DE8EDF000E6E75F /* CapturingAdapterErrorHandler.swift in Sources */, - 98E5642D2DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */, 9FA0AF212EB8358100713387 /* MockWhatsNewDisplayModelMapper.swift in Sources */, 98E5642E2DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */, 9F387DF92EA8BF4F006861F8 /* MockLaunchSourceManager.swift in Sources */, @@ -15639,7 +15578,6 @@ 98E563C22DE8B32D00E6E75F /* MockTextZoomCoordinator.swift in Sources */, 8544C9872EDDE952001E1DB4 /* MockProductSurfaceTelemetry.swift in Sources */, 98E563C42DE8B32D00E6E75F /* MockWebsiteDataManager.swift in Sources */, - 98E563C52DE8B32D00E6E75F /* OnboardingFirePixelMock.swift in Sources */, 9F387E0F2EA9A30B006861F8 /* MockNewAddressBarPickerStorage.swift in Sources */, 98E563C62DE8B32D00E6E75F /* MockFaviconStore.swift in Sources */, 98E563C72DE8B32D00E6E75F /* MockFireproofing.swift in Sources */, @@ -15665,7 +15603,6 @@ 98E563CF2DE8B32D00E6E75F /* OnboardingHostingControllerMock.swift in Sources */, 98E563D02DE8B32D00E6E75F /* MockOmniBar.swift in Sources */, 98E563D12DE8B32D00E6E75F /* MockUsageSegmentation.swift in Sources */, - 98E563D22DE8B32D00E6E75F /* MockPixelFiring.swift in Sources */, 98E563D32DE8B32D00E6E75F /* MockURLOpener.swift in Sources */, 98E563D42DE8B32D00E6E75F /* MockPrivacyDataReporter.swift in Sources */, 98E564AD2DE8EDF000E6E75F /* CapturingAdapterErrorHandler.swift in Sources */, @@ -15680,7 +15617,6 @@ C1AITEST2F2E000500E35AD5 /* AIChatPageContextHandlerTests.swift in Sources */, 80C947FD2F53E819002EDB79 /* Tab+ConvenienceInitializer.swift in Sources */, 98E563DC2DE8B32D00E6E75F /* MockDDGSyncing.swift in Sources */, - 98E563DD2DE8B32D00E6E75F /* MockPersistentPixel.swift in Sources */, 98E563DE2DE8B32D00E6E75F /* MockTabDelegate.swift in Sources */, 98E563DF2DE8B32D00E6E75F /* MockTutorialSettings.swift in Sources */, 9F3668782EAF0A7600AA8670 /* MockContextualOnboardingStatusProvider.swift in Sources */, @@ -15736,9 +15672,7 @@ 989F0DA02ECB278600781641 /* ContentBlockerRulesManagerMock.swift in Sources */, 9876DF2C2DEEE21700E30713 /* MockThemeManager.swift in Sources */, 9FA0AF222EB8358100713387 /* MockWhatsNewDisplayModelMapper.swift in Sources */, - 98E564522DE8DDDC00E6E75F /* OnboardingFirePixelMock.swift in Sources */, 986CD5402DE8FA1400AFA98A /* ConfigurationMocks.swift in Sources */, - 98E564532DE8DDDC00E6E75F /* MockPersistentPixel.swift in Sources */, 98E564552DE8DDDC00E6E75F /* MockFireproofing.swift in Sources */, 989F0DAC2ECB517100781641 /* MockDefaultScriptSourceProviderDependencies.swift in Sources */, 98E564562DE8DDDC00E6E75F /* MockTabPreviewsSource.swift in Sources */, @@ -15750,7 +15684,6 @@ 9F60CBB62E989EA4004D2367 /* MockRemoteMessagingPresenter.swift in Sources */, 9F6454292F99CB8200D5FB71 /* MockCustomProductPageEvaluator.swift in Sources */, 98E564592DE8DDDC00E6E75F /* MockDDGSyncing.swift in Sources */, - 98E5645A2DE8DDDC00E6E75F /* MockPixelFiring.swift in Sources */, 98E5645B2DE8DDDC00E6E75F /* MockUsageSegmentation.swift in Sources */, 98E564A62DE8EDE200E6E75F /* CapturingAlertPresenter.swift in Sources */, 986CD5482DE9134700AFA98A /* ContextualDaxDialogsFactoryTests.swift in Sources */, @@ -15873,7 +15806,6 @@ 858479C92B8792D800D156C1 /* HistoryManager.swift in Sources */, C1CAAAA32CFCBBBD00C37EE6 /* UserAuthenticator.swift in Sources */, F1134EAB1F3E2C6A00B73467 /* StatisticsUserDefaults.swift in Sources */, - 4B27FBB32C926E51007E21A7 /* PersistentPixel.swift in Sources */, CB258D1E29A52AF900DEBA24 /* FileStore.swift in Sources */, F1075C921E9EF827006BE8A8 /* UserDefaultsExtension.swift in Sources */, C1C23C102D490CA500B6BDF6 /* ImportArchiveReader.swift in Sources */, @@ -15902,14 +15834,11 @@ 85A1B3B220C6CD9900C18F15 /* MigratableCookieStorage.swift in Sources */, 9856A1992933D2EB00ACB44F /* BookmarksModelsErrorHandling.swift in Sources */, 850559D023CF647C0055C0D5 /* Fireproofing.swift in Sources */, - 4B27FBAE2C924EC6007E21A7 /* PersistentPixelStoring.swift in Sources */, - 6F7FB8E32C660BF300867DA7 /* DailyPixelFiring.swift in Sources */, CCB36B112E5635F300DBFE3A /* UserScriptError+Pixel.swift in Sources */, F1134EB51F40AEEA00B73467 /* StatisticsLoader.swift in Sources */, CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */, EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */, CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */, - BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */, 98629D312C21765A001E6031 /* BookmarksStateValidation.swift in Sources */, 9847C00027A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift in Sources */, F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */, @@ -15918,7 +15847,6 @@ 4B60ACA1252EC0B100E8D219 /* FullScreenVideoUserScript.swift in Sources */, F1A886781F29394E0096251E /* WebCacheManager.swift in Sources */, C1CAAA9C2CFCB39800C37EE6 /* AutofillLoginListSorting.swift in Sources */, - 6F03CB072C32F173004179A8 /* PixelFiring.swift in Sources */, 362AB8382F69A33700E3F222 /* ActionResult.swift in Sources */, C14882DA27F2011C00D59F0C /* BookmarksExporter.swift in Sources */, 8598D2DC2CEB93AD00C45685 /* FaviconsCacheType.swift in Sources */, @@ -15939,7 +15867,6 @@ 836A941D247F23C600BF8EF5 /* UserAgentManaging.swift in Sources */, F143C3181E4A99D200CFDE3A /* Link.swift in Sources */, EE00D2D82DFC3A3300B79E9D /* UserDefaults+Autofill.swift in Sources */, - 6F03CB092C32F331004179A8 /* PixelFiringAsync.swift in Sources */, CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */, 31052848516C942B35E59579 /* PixelEvent+PixelKit.swift in Sources */, DA19AC9ABF6B4AD98E1887EE /* LegacyPixelStateMigration.swift in Sources */, @@ -15952,7 +15879,6 @@ 85A5FEE62ED9D88D001B5605 /* ProductSurfaceTelemetry.swift in Sources */, F1D477CB1F2149C40031ED49 /* Type.swift in Sources */, 983C52E42C2C050B007B5747 /* BookmarksStateRepair.swift in Sources */, - 1E05D1D629C46EBB00BF9A1F /* DailyPixel.swift in Sources */, 379E877429E97C8D001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */, C19D90D12CFE3A7F00D17DF3 /* AutofillLoginListSectionType.swift in Sources */, F1134EA61F3E2AF400B73467 /* StatisticsStore.swift in Sources */, diff --git a/iOS/DuckDuckGo/AppLifecycle/AppDependencyProvider.swift b/iOS/DuckDuckGo/AppLifecycle/AppDependencyProvider.swift index d878a46e0f0..1713a173d67 100644 --- a/iOS/DuckDuckGo/AppLifecycle/AppDependencyProvider.swift +++ b/iOS/DuckDuckGo/AppLifecycle/AppDependencyProvider.swift @@ -59,7 +59,6 @@ protocol DependencyProvider { var serverInfoObserver: ConnectionServerInfoObserver { get } var connectionErrorObserver: ConnectionErrorObserver { get } var vpnSettings: VPNSettings { get } - var persistentPixel: PersistentPixelFiring { get } var wideEvent: WideEventManaging { get } var freeTrialConversionService: FreeTrialConversionInstrumentationService { get } var subscriptionManager: any SubscriptionManager { get } @@ -107,7 +106,6 @@ final class AppDependencyProvider: DependencyProvider { lazy var connectionErrorObserver: ConnectionErrorObserver = ConnectionErrorObserverThroughSession() let vpnSettings = VPNSettings(defaults: .networkProtectionGroupDefaults) let dbpSettings = DataBrokerProtectionSettings(defaults: .dbp) - let persistentPixel: PersistentPixelFiring = PersistentPixel() let wideEvent: WideEventManaging let freeTrialConversionService: FreeTrialConversionInstrumentationService lazy var syncAutoRestoreDecisionManager: SyncAutoRestoreDecisionManaging = SyncAutoRestoreDecisionManager(featureFlagger: featureFlagger) @@ -319,7 +317,6 @@ final class AppDependencyProvider: DependencyProvider { vpnFeatureVisibility = DefaultNetworkProtectionVisibility(authenticationStateProvider: authenticationStateProvider) networkProtectionTunnelController = NetworkProtectionTunnelController(tokenHandler: tokenHandler, featureFlagger: featureFlagger, - persistentPixel: persistentPixel, settings: vpnSettings, wideEvent: wideEvent, freeTrialConversionService: freeTrialConversionService diff --git a/iOS/DuckDuckGo/AppServices/ReportingService.swift b/iOS/DuckDuckGo/AppServices/ReportingService.swift index 6e3132a0a63..cfe59f0d850 100644 --- a/iOS/DuckDuckGo/AppServices/ReportingService.swift +++ b/iOS/DuckDuckGo/AppServices/ReportingService.swift @@ -181,7 +181,6 @@ final class ReportingService { await subscriptionDataReporter.saveWidgetAdded() } reportFailedCompilationsPixelIfNeeded() - AppDependencyProvider.shared.persistentPixel.sendQueuedPixels { _ in } } // MARK: - Suspend diff --git a/iOS/DuckDuckGo/DefaultBrowserPrompt/EventMappers/DefaultBrowserPromptManagerDebugPixelHandler.swift b/iOS/DuckDuckGo/DefaultBrowserPrompt/EventMappers/DefaultBrowserPromptManagerDebugPixelHandler.swift index 579e8f008d0..a032d549ec8 100644 --- a/iOS/DuckDuckGo/DefaultBrowserPrompt/EventMappers/DefaultBrowserPromptManagerDebugPixelHandler.swift +++ b/iOS/DuckDuckGo/DefaultBrowserPrompt/EventMappers/DefaultBrowserPromptManagerDebugPixelHandler.swift @@ -20,7 +20,7 @@ import Foundation import PixelKit import class Common.EventMapping -import class Core.Pixel +import enum Core.Pixel import SetDefaultBrowserCore final class DefaultBrowserPromptManagerDebugPixelHandler: EventMapping, DefaultBrowserPromptEventMapping { diff --git a/iOS/DuckDuckGo/NetworkProtectionTunnelController.swift b/iOS/DuckDuckGo/NetworkProtectionTunnelController.swift index 6715934279a..a450de75354 100644 --- a/iOS/DuckDuckGo/NetworkProtectionTunnelController.swift +++ b/iOS/DuckDuckGo/NetworkProtectionTunnelController.swift @@ -47,7 +47,7 @@ final class NetworkProtectionTunnelController: VPNConnectionContextProvidingTunn private let snoozeTimingStore = NetworkProtectionSnoozeTimingStore(userDefaults: .networkProtectionGroupDefaults) private let notificationCenter: NotificationCenter = .default private var previousStatus: NEVPNStatus = .invalid - private let persistentPixel: PersistentPixelFiring + private let pixelFiring: (any PixelKitFiring)? private let settings: VPNSettings private lazy var startupMonitor = VPNStartupMonitor() private var cancellables = Set() @@ -176,14 +176,14 @@ final class NetworkProtectionTunnelController: VPNConnectionContextProvidingTunn init(tokenHandler: any SubscriptionTokenHandling, featureFlagger: FeatureFlagger, - persistentPixel: PersistentPixelFiring, + pixelFiring: (any PixelKitFiring)? = PixelKit.shared, settings: VPNSettings, wideEvent: WideEventManaging, freeTrialConversionService: FreeTrialConversionInstrumentationService ) { self.featureFlagger = featureFlagger - self.persistentPixel = persistentPixel + self.pixelFiring = pixelFiring self.settings = settings self.tokenHandler = tokenHandler self.wideEvent = wideEvent @@ -208,23 +208,13 @@ final class NetworkProtectionTunnelController: VPNConnectionContextProvidingTunn private func start(with entryContext: VPNConnectionWideEventData.EntryContext?) async { setupAndStartConnectionWideEvent(entryContext: entryContext) controllerErrorSubject.send(nil) - persistentPixel.fire( - pixel: .networkProtectionControllerStartAttempt, - error: nil, - includedParameters: [.appVersion], - withAdditionalParameters: [:], - onComplete: { _ in }) + pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartAttempt, options: .withRetry) do { try await startWithError() completeAndCleanupConnectionWideEvent() - persistentPixel.fire( - pixel: .networkProtectionControllerStartSuccess, - error: nil, - includedParameters: [.appVersion], - withAdditionalParameters: [:], - onComplete: { _ in }) + pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartSuccess, options: .withRetry) } catch { if let message = userFacingControllerErrorMessage(for: error) { controllerErrorSubject.send(message) @@ -236,12 +226,7 @@ final class NetworkProtectionTunnelController: VPNConnectionContextProvidingTunn return } - persistentPixel.fire( - pixel: .networkProtectionControllerStartFailure, - error: error, - includedParameters: [.appVersion], - withAdditionalParameters: [:], - onComplete: { _ in }) + pixelFiring?.fire(Pixel.Event.networkProtectionControllerStartFailure.withError(error), options: .withRetry) #if DEBUG errorStore.lastErrorMessage = error.localizedDescription diff --git a/iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Components/OnboardingSubscriptionPromotionHelper.swift b/iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Components/OnboardingSubscriptionPromotionHelper.swift index 8d59a376fa8..1f44fbf8ff7 100644 --- a/iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Components/OnboardingSubscriptionPromotionHelper.swift +++ b/iOS/DuckDuckGo/OnboardingFlow/ContextualOnboarding/Components/OnboardingSubscriptionPromotionHelper.swift @@ -88,7 +88,7 @@ struct OnboardingSubscriptionPromotionHelper: OnboardingSubscriptionPromotionHel /// - Parameters: /// - featureFlagger: The feature flagging service. Defaults to the shared instance. /// - subscriptionManager: The subscription manager. Defaults to the shared instance. - /// - pixelFiring: The pixel firing service. Defaults to Pixel.self. + /// - pixelFiring: The pixel firing service. Defaults to PixelKit.shared. /// - statisticsStore: The statistics store. Defaults to StatisticsUserDefaults. init(featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, subscriptionManager: any SubscriptionManager = AppDependencyProvider.shared.subscriptionManager, diff --git a/iOS/DuckDuckGo/OnboardingFlow/Pixels/OnboardingPixelReporter.swift b/iOS/DuckDuckGo/OnboardingFlow/Pixels/OnboardingPixelReporter.swift index 4b6e04ca27b..3ab44f1eedd 100644 --- a/iOS/DuckDuckGo/OnboardingFlow/Pixels/OnboardingPixelReporter.swift +++ b/iOS/DuckDuckGo/OnboardingFlow/Pixels/OnboardingPixelReporter.swift @@ -27,24 +27,6 @@ import PixelKit import PixelExperimentKit import FeatureFlags_iOS -// MARK: - Pixel Fire Interface - -protocol OnboardingPixelFiring { - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) -} - -extension Pixel: OnboardingPixelFiring { - static func fire(pixel: Event, withAdditionalParameters params: [String: String], includedParameters: [QueryParameters]) { - self.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: { _ in }) - } -} - -extension UniquePixel: OnboardingPixelFiring { - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { - self.fire(pixel: pixel, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: { _ in }) - } -} - // MARK: - OnboardingPixelReporter protocol OnboardingIntroImpressionReporting { @@ -148,8 +130,7 @@ typealias OnboardingPixelReporting = LinearOnboardingPixelReporting & Onboarding // MARK: - Implementation final class OnboardingPixelReporter { - private let pixel: OnboardingPixelFiring.Type - private let uniquePixel: OnboardingPixelFiring.Type + private let pixelFiring: (any PixelKitFiring)? private let statisticsStore: StatisticsStore private let calendar: Calendar private let dateProvider: () -> Date @@ -160,8 +141,7 @@ final class OnboardingPixelReporter { private let downloadReasonExperimentMetric: OnboardingDownloadReasonExperimentMetric init( - pixel: OnboardingPixelFiring.Type = Pixel.self, - uniquePixel: OnboardingPixelFiring.Type = UniquePixel.self, + pixelFiring: (any PixelKitFiring)? = PixelKit.shared, statisticsStore: StatisticsStore = StatisticsUserDefaults(), calendar: Calendar = .current, dateProvider: @escaping () -> Date = Date.init, @@ -170,8 +150,7 @@ final class OnboardingPixelReporter { sharedPixelsStorage: (any KeyedStoring)? = nil, downloadReasonExperimentMetric: OnboardingDownloadReasonExperimentMetric = OnboardingDownloadReasonExperimentMetric() ) { - self.pixel = pixel - self.uniquePixel = uniquePixel + self.pixelFiring = pixelFiring self.statisticsStore = statisticsStore self.calendar = calendar self.dateProvider = dateProvider @@ -185,12 +164,11 @@ final class OnboardingPixelReporter { self.downloadReasonExperimentMetric = downloadReasonExperimentMetric } - private func fire(event: Pixel.Event, unique: Bool, additionalParameters: [String: String] = [:], includedParameters: [Pixel.QueryParameters] = [.appVersion]) { - if unique { - uniquePixel.fire(pixel: event, withAdditionalParameters: additionalParameters, includedParameters: includedParameters) - } else { - pixel.fire(pixel: event, withAdditionalParameters: additionalParameters, includedParameters: includedParameters) - } + 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). + pixelFiring?.fire(event, frequency: unique ? .legacyInitial : .standard, options: .parameters(additionalParameters)) } // Fires a shared onboarding pixel with the current stored context (source, flow and variant). diff --git a/iOS/DuckDuckGo/PixelKitFiring.swift b/iOS/DuckDuckGo/PixelKitFiring.swift deleted file mode 100644 index 438bde1e6f0..00000000000 --- a/iOS/DuckDuckGo/PixelKitFiring.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// PixelKitFiring.swift -// DuckDuckGo -// -// Copyright © 2026 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import PixelKit - -/// PixelKit's `PixelFiring`, under a name that does not collide with `Core.PixelFiring`. -/// -/// Any file importing both modules sees an ambiguous `PixelFiring`. Once `Core.PixelFiring` is -/// deleted the ambiguity goes with it and this typealias can be renamed away. -typealias PixelKitFiring = PixelFiring diff --git a/iOS/DuckDuckGo/Subscription/FreeTrials/VPNSubscriptionPromotionHelper.swift b/iOS/DuckDuckGo/Subscription/FreeTrials/VPNSubscriptionPromotionHelper.swift index 43b67f46656..8628e00f61c 100644 --- a/iOS/DuckDuckGo/Subscription/FreeTrials/VPNSubscriptionPromotionHelper.swift +++ b/iOS/DuckDuckGo/Subscription/FreeTrials/VPNSubscriptionPromotionHelper.swift @@ -81,7 +81,7 @@ struct VPNSubscriptionPromotionHelper: VPNSubscriptionPromotionHelping { /// - featureFlagger: The feature flagging service. Defaults to the shared instance. /// - subscriptionManager: The subscription manager. Defaults to the shared instance. /// - freeTrialBadgePersistor: The persistor for tracking promotion views. Defaults to an instance using UserDefaults and a custom key prefix. - /// - pixelFiring: The pixel firing service. Defaults to Pixel.self. + /// - pixelFiring: The pixel firing service. Defaults to PixelKit.shared. init(featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger, subscriptionManager: any SubscriptionManager = AppDependencyProvider.shared.subscriptionManager, freeTrialBadgePersistor: FreeTrialBadgePersisting = FreeTrialBadgePersistor(keyValueStore: UserDefaults.standard, keyPrefix: "vpn-menu-item"), diff --git a/iOS/DuckDuckGoTests/DailyPixelTests.swift b/iOS/DuckDuckGoTests/DailyPixelTests.swift deleted file mode 100644 index 9b6ea282ada..00000000000 --- a/iOS/DuckDuckGoTests/DailyPixelTests.swift +++ /dev/null @@ -1,432 +0,0 @@ -// -// DailyPixelTests.swift -// DuckDuckGo -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import Networking -@_spi(Testing) import Persistence -@testable import Core - -final class DailyPixelTests: XCTestCase { - - let mockStore = MockKeyValueStore() - - override func tearDown() { - super.tearDown() - - PixelFiringMock.tearDown() - } - - func testThatDailyPixelFiresCorrectlyForTheFirstTime() { - let expectation = XCTestExpectation() - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelFiresForTheFirstTimeButNotForTheSecond() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 2 - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNotNil(error) - XCTAssertEqual(error as? DailyPixel.Error, .alreadyFired) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWillFireIfFiredPreviouslyOnDifferentDay() { - let expectation = XCTestExpectation() - - updateLastFireDateToYesterday(for: .forgetAllPressedBrowsing) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithSameErrorFiresForTheFirstTimeButNotForTheSecond() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 2 - - let error = NSError(domain: "test", code: 0, userInfo: nil) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNotNil(error) - XCTAssertEqual(error as? DailyPixel.Error, .alreadyFired) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.error as? NSError, error) - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithTwoDifferentErrorsBothFireFirstTime() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 2 - - let error1 = NSError(domain: "test1", code: 1, userInfo: nil) - let error2 = NSError(domain: "test2", code: 2, userInfo: nil) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error1, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.error as? NSError, error1) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error2, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.error as? NSError, error2) - - wait(for: [expectation], timeout: 3.0) - } - - - func testThatDailyPixelWithTwoDifferentErrorsBothFireFirstTimeButNotForTheSecond() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 4 - - let error1 = NSError(domain: "test1", code: 1, userInfo: nil) - let error2 = NSError(domain: "test1", code: 2, userInfo: nil) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error1, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.error as? NSError, error1) - - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error2, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - XCTAssertEqual(PixelFiringMock.lastPixelName, Pixel.Event.forgetAllPressedBrowsing.name) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.error as? NSError, error2) - - PixelFiringMock.tearDown() - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error1, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNotNil(error) - XCTAssertEqual(error as? DailyPixel.Error, .alreadyFired) - expectation.fulfill() - } - - XCTAssertNil(PixelFiringMock.lastPixelName) - - DailyPixel.fire(pixel: .forgetAllPressedBrowsing, - error: error2, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore) { error in - XCTAssertNotNil(error) - XCTAssertEqual(error as? DailyPixel.Error, .alreadyFired) - expectation.fulfill() - } - - XCTAssertNil(PixelFiringMock.lastPixelName) - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithCountFiresCorrectlyForTheFirstTime() { - let countExpectation = XCTestExpectation() - let dailyExpectation = XCTestExpectation() - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onDailyComplete: { error in - XCTAssertNil(error) - dailyExpectation.fulfill() - - }, - onCountComplete: { error in - XCTAssertNil(error) - countExpectation.fulfill() - } - ) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - - wait(for: [countExpectation, dailyExpectation], timeout: 3.0) - } - - func testThatDailyPixelWithCount_DailyFiresForTheFirstTimeButNotForTheSecond() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 2 - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onDailyComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onDailyComplete: { error in - XCTAssertNotNil(error) - XCTAssertEqual(error as? DailyPixel.Error, .alreadyFired) - expectation.fulfill() - } - ) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 3) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - XCTAssertEqual(PixelFiringMock.allPixelsFired[2].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithCountBubblesUpNetworkErrors() { - let countExpectation = XCTestExpectation() - let dailyExpectation = XCTestExpectation() - - PixelFiringMock.expectedFireError = TestError.testError - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onDailyComplete: { error in - XCTAssertNotNil(error) - dailyExpectation.fulfill() - }, - onCountComplete: { error in - XCTAssertNotNil(error) - countExpectation.fulfill() - } - ) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - - wait(for: [countExpectation, dailyExpectation], timeout: 3.0) - } - - func testThatDailyPixelWithCount_CountFiresBothTimes() { - let expectation = XCTestExpectation() - expectation.expectedFulfillmentCount = 2 - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onCountComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onCountComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 3) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - XCTAssertEqual(PixelFiringMock.allPixelsFired[2].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithCountWillFireIfFiredPreviouslyOnDifferentDay() { - let expectation = XCTestExpectation() - - updateLastFireDateToYesterday(for: .forgetAllPressedBrowsing) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onDailyComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - - wait(for: [expectation], timeout: 3.0) - } - - func testThatDailyPixelWithLegacyPixelSuffixAndCountWillAppendDAndC() { - let expectation = XCTestExpectation() - - updateLastFireDateToYesterday(for: .forgetAllPressedBrowsing) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onCountComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - wait(for: [expectation], timeout: 3.0) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_d") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_c") - } - - func testThatDailyPixelWithModernPixelSuffixesWillAppendDailyAndCount() { - let expectation = XCTestExpectation() - - updateLastFireDateToYesterday(for: .forgetAllPressedBrowsing) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelNameSuffixes: DailyPixel.Constant.dailyPixelSuffixes, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onCountComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - wait(for: [expectation], timeout: 3.0) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_daily") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_count") - } - - func testThatDailyPixelWithDefaultPixelSuffixesWillAppendDailyAndCount() { - let expectation = XCTestExpectation() - - updateLastFireDateToYesterday(for: .forgetAllPressedBrowsing) - - DailyPixel.fireDailyAndCount( - pixel: .forgetAllPressedBrowsing, - pixelFiring: PixelFiringMock.self, - dailyPixelStore: mockStore, - onCountComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - ) - - wait(for: [expectation], timeout: 3.0) - - XCTAssertEqual(PixelFiringMock.allPixelsFired.count, 2) - XCTAssertEqual(PixelFiringMock.allPixelsFired[0].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_daily") - XCTAssertEqual(PixelFiringMock.allPixelsFired[1].pixelName, Pixel.Event.forgetAllPressedBrowsing.name + "_count") - } - - private func updateLastFireDateToYesterday(for pixel: Pixel.Event) { - let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date()) - mockStore.set(yesterday, forKey: pixel.name) - } - - private enum TestError: Error { - case testError - } -} diff --git a/iOS/DuckDuckGoTests/DefaultPersistentPixelStorageTests.swift b/iOS/DuckDuckGoTests/DefaultPersistentPixelStorageTests.swift deleted file mode 100644 index 7ddb6077f91..00000000000 --- a/iOS/DuckDuckGoTests/DefaultPersistentPixelStorageTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// DefaultPersistentPixelStorageTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import XCTest -@testable import Core - -class DefaultPersistentPixelStorageTests: XCTestCase { - - var currentStorageURL: URL! - var persistentStorage: DefaultPersistentPixelStorage! - - override func setUp() { - super.setUp() - let (url, storage) = createPersistentStorage() - self.currentStorageURL = url - self.persistentStorage = storage - } - - override func tearDown() { - super.tearDown() - try? FileManager.default.removeItem(at: currentStorageURL) - } - - func testWhenStoringPixel_ThenPixelCanBeSuccessfullyRead() throws { - let metadata = event(named: "test", parameters: ["param": "value"]) - try persistentStorage.append(pixels: [metadata]) - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual([metadata], storedPixels) - } - - func testWhenStoringMultiplePixels_ThenPixelsCanBeSuccessfullyRead() throws { - let metadata1 = event(named: "test1", parameters: ["param1": "value1"]) - let metadata2 = event(named: "test2", parameters: ["param2": "value2"]) - let metadata3 = event(named: "test3", parameters: ["param3": "value3"]) - - try persistentStorage.append(pixels: [metadata1, metadata2, metadata3]) - - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual([metadata1, metadata2, metadata3], storedPixels) - } - - func testWhenStoringMorePixelsThanTheLimit_AndPixelsAreAddedIncrementally_ThenOldPixelsAreDropped() throws { - for index in 1...(DefaultPersistentPixelStorage.Constants.pixelCountLimit + 50) { - let metadata = event(named: "pixel\(index)", parameters: ["param\(index)": "value\(index)"]) - try persistentStorage.append(pixels: [metadata]) - } - - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual(storedPixels.count, DefaultPersistentPixelStorage.Constants.pixelCountLimit) - XCTAssertEqual(storedPixels.first?.eventName, "pixel51") - XCTAssertEqual(storedPixels.last?.eventName, "pixel150") - } - - func testWhenStoringMorePixelsThanTheLimit_AndPixelsAreAddedInASingleBatch_ThenOldPixelsAreDropped() throws { - var pixelsToAdd: [PersistentPixelMetadata] = [] - - for index in 1...(DefaultPersistentPixelStorage.Constants.pixelCountLimit + 50) { - let metadata = event(named: "pixel\(index)", parameters: ["param\(index)": "value\(index)"]) - pixelsToAdd.append(metadata) - } - - try persistentStorage.append(pixels: pixelsToAdd) - - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual(storedPixels.count, DefaultPersistentPixelStorage.Constants.pixelCountLimit) - XCTAssertEqual(storedPixels.first?.eventName, "pixel51") - XCTAssertEqual(storedPixels.last?.eventName, "pixel150") - } - - func testWhenRemovingPixels_AndNoPixelsAreStored_ThenNothingHappens() throws { - try persistentStorage.remove(pixelsWithIDs: Set([UUID()])) - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual([], storedPixels) - } - - func testWhenRemovingPixels_AndIDDoesNotMatchStoredPixel_ThenNothingHappens() throws { - let metadata = event(named: "test", parameters: ["param": "value"]) - try persistentStorage.append(pixels: [metadata]) - try persistentStorage.remove(pixelsWithIDs: Set([UUID()])) - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual([metadata], storedPixels) - } - - func testWhenRemovingPixels_AndIDMatchesStoredPixel_ThenPixelIsRemoved() throws { - let metadata = event(named: "test", parameters: ["param": "value"]) - try persistentStorage.remove(pixelsWithIDs: Set([metadata.id])) - let storedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual([], storedPixels) - } - - - // MARK: - Test Utilities - - private func createPersistentStorage() -> (URL, DefaultPersistentPixelStorage) { - let storageDirectory = FileManager.default.temporaryDirectory - let fileName = UUID().uuidString.appendingPathExtension("json") - - return ( - storageDirectory.appendingPathComponent(fileName), - DefaultPersistentPixelStorage(fileName: fileName, storageDirectory: storageDirectory) - ) - } - - private func event(named name: String, parameters: [String: String]) -> PersistentPixelMetadata { - return PersistentPixelMetadata(eventName: name, additionalParameters: parameters, includedParameters: [.appVersion]) - } - -} diff --git a/iOS/DuckDuckGoTests/OnboardingPixelReporterTests.swift b/iOS/DuckDuckGoTests/OnboardingPixelReporterTests.swift index def37f97104..b4ad67876cc 100644 --- a/iOS/DuckDuckGoTests/OnboardingPixelReporterTests.swift +++ b/iOS/DuckDuckGoTests/OnboardingPixelReporterTests.swift @@ -21,6 +21,7 @@ import XCTest import Core import Onboarding @_spi(Testing) import Persistence +@_spi(Testing) import PixelKit import PrivacyConfig import PixelExperimentKit @testable import DuckDuckGo @@ -28,6 +29,7 @@ import PixelExperimentKit final class OnboardingPixelReporterTests: XCTestCase { private static let suiteName = "testing_onboarding_pixel_store" private var sut: OnboardingPixelReporter! + private var pixelKitMock: PixelKitMock! private var statisticsStoreMock: MockStatisticsStore! private var now: Date! private var userDefaultsMock: UserDefaults! @@ -35,6 +37,7 @@ final class OnboardingPixelReporterTests: XCTestCase { private var sharedPixelsStorageMock: (any KeyedStoring)! override func setUpWithError() throws { + pixelKitMock = PixelKitMock() statisticsStoreMock = MockStatisticsStore() statisticsStoreMock.atb = "TESTATB" now = Date() @@ -44,13 +47,12 @@ final class OnboardingPixelReporterTests: XCTestCase { sharedPixelHandlerMock = MockOnboardingSharedPixelHandling() initSharedPixelsStorageMock() MockExperimentPixelFiring.reset() - sut = OnboardingPixelReporter(pixel: OnboardingPixelFireMock.self, uniquePixel: OnboardingUniquePixelFireMock.self, statisticsStore: statisticsStoreMock, calendar: calendar, dateProvider: { self.now }, userDefaults: userDefaultsMock, sharedPixelHandler: sharedPixelHandlerMock, sharedPixelsStorage: sharedPixelsStorageMock, downloadReasonExperimentMetric: OnboardingDownloadReasonExperimentMetric(experimentPixelFiring: MockExperimentPixelFiring.self)) + sut = OnboardingPixelReporter(pixelFiring: pixelKitMock, statisticsStore: statisticsStoreMock, calendar: calendar, dateProvider: { self.now }, userDefaults: userDefaultsMock, sharedPixelHandler: sharedPixelHandlerMock, sharedPixelsStorage: sharedPixelsStorageMock, downloadReasonExperimentMetric: OnboardingDownloadReasonExperimentMetric(experimentPixelFiring: MockExperimentPixelFiring.self)) try super.setUpWithError() } override func tearDownWithError() throws { - OnboardingPixelFireMock.tearDown() - OnboardingUniquePixelFireMock.tearDown() + pixelKitMock = nil sharedPixelsStorageMock = nil sharedPixelHandlerMock = nil statisticsStoreMock = nil @@ -72,21 +74,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureOnboardingIntroImpressionThenLegacyIntroShownUniqueAndWelcomeShownPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroShownUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureOnboardingIntroImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_intro_shown_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.welcome(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -97,21 +97,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureSkipOnboardingCTAIsCalledThenLegacySkipPressedAndWelcomeDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroSkipOnboardingCTAPressed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureSkipOnboardingCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_skip-onboarding-pressed") - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.welcome(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -122,21 +120,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureConfirmSkipOnboardingCTAIsCalledThenLegacyConfirmSkipPressedAndSkipOnboardingEngageSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroConfirmSkipOnboardingCTAPressed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureConfirmSkipOnboardingCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_confirm-skip-onboarding-pressed") - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.skipOnboarding(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -147,21 +143,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureCancelSkipOnboardingCTAIsCalledThenLegacyResumePressedAndSkipOnboardingDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroResumeOnboardingCTAPressed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureResumeOnboardingCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_resume-onboarding-pressed") - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.skipOnboarding(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -172,21 +166,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureBrowserComparisonImpressionThenLegacyComparisonChartShownUniqueAndSetDefaultShownSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroComparisonChartShownUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureSetDefaultBrowserImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_comparison_chart_shown_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.setDefault(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -197,21 +189,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureChooseBrowserCTAActionThenLegacyChooseBrowserPressedAndSetDefaultEngageSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroChooseBrowserCTAPressed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseBrowserCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_preonboarding_choose_browser_pressed") - XCTAssertEqual(OnboardingPixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.setDefault(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -222,15 +212,15 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAiComparisonImpressionThenAiComparisonShownSharedPixelFires() { // GIVEN XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) // WHEN sut.measureAiIntroImpression() // THEN - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.aiIntro(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) XCTAssertEqual(sharedPixelHandlerMock.receivedFlow, .duckAI) @@ -240,15 +230,15 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAiComparisonCTAActionThenAiComparisonEngageSharedPixelFires() { // GIVEN XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) // WHEN sut.measureAiIntroCTAAction() // THEN - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.aiIntro(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) XCTAssertEqual(sharedPixelHandlerMock.receivedFlow, .duckAI) @@ -258,15 +248,15 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureStartOnboardingCTAActionThenWelcomeEngageSharedPixelFires() { // GIVEN XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) // WHEN sut.measureStartOnboardingCTAAction() // THEN - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.welcome(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -282,8 +272,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureAutoRestoreOnboardingRestoreCTAAction() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) } func testWhenMeasureAutoRestoreOnboardingSkipCTAActionThenLegacySkipTappedUniquePixelFires() { @@ -294,8 +284,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureAutoRestoreOnboardingSkipCTAAction() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) } func testWhenMeasureAutoRestoreOnboardingPromptShownThenLegacyPixelFiresWithoutSharedPixels() { @@ -307,8 +297,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureAutoRestoreOnboardingPromptShown() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertTrue(sharedPixelHandlerMock.eventsFired.isEmpty) } @@ -345,21 +335,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureCustomSearchIsCalledThenLegacySearchCustomUniqueAndSearchCustomSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingContextualSearchCustomUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureCustomSearch() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_onboarding_search_custom_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.search(.clicked(.custom))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -370,21 +358,19 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureCustomSiteIsCalledThenLegacySiteCustomUniqueAndVisitSiteCustomSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingContextualSiteCustomUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureCustomSite() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_onboarding_visit_site_custom_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.visitSite(.clicked(.custom))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -395,19 +381,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureSecondVisitIsCalledAndStoreDoesNotContainPixelThenPixelIsNotFired() { // GIVEN XCTAssertNil(userDefaultsMock.value(forKey: "com.duckduckgo.ios.site-visited")) - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) // WHEN sut.measureSecondSiteVisit() // THEN - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) } func testWhenMeasureSecondVisitIsCalledThenFiresOnlyOnSecondTime() { @@ -416,48 +400,44 @@ final class OnboardingPixelReporterTests: XCTestCase { userDefaultsMock.set(true, forKey: key) XCTAssertTrue(userDefaultsMock.bool(forKey: key)) let expectedPixel = Pixel.Event.onboardingContextualSecondSiteVisitUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) // WHEN sut.measureSecondSiteVisit() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_second_sitevisit_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) } func testWhenMeasurePrivacyDashboardOpenedForFirstTimeThenPrivacyDashboardFirstTimeOpenedPixelFires() { // GIVEN let expectedPixel = Pixel.Event.privacyDashboardFirstTimeOpenedUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) // WHEN sut.measurePrivacyDashboardOpenedForFirstTime() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, "m_privacy_dashboard_first_time_used_unique") - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) } func testWhenMeasurePrivacyDashboardOpenedForFirstTimeThenFromOnboardingParameterIsSetToTrue() { // GIVEN - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) // WHEN sut.measurePrivacyDashboardOpenedForFirstTime() // THEN - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams["from_onboarding"], "true") + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters?["from_onboarding"], "true") } func testWhenMeasurePrivacyDashboardOpenedForFirstTimeThenDaysSinceInstallParameterIsSet() { @@ -465,13 +445,13 @@ final class OnboardingPixelReporterTests: XCTestCase { let installDate = Date(timeIntervalSince1970: 1722348000) // 30th July 2024 GMT now = Date(timeIntervalSince1970: 1722607200) // 1st August 2024 GMT statisticsStoreMock.installDate = installDate - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams, [:]) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters ?? [:], [:]) // WHEN sut.measurePrivacyDashboardOpenedForFirstTime() // THEN - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedParams["daysSinceInstall"], "3") + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.additionalParameters?["daysSinceInstall"], "3") } // MARK: - Dax Dialogs @@ -479,18 +459,16 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureScreenImpressionIsCalledThenLegacyUniquePixelFires() { // GIVEN let expectedPixel = Pixel.Event.daxDialogsSerpUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) // WHEN sut.measureScreenImpression(event: expectedPixel) // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertTrue(sharedPixelHandlerMock.eventsFired.isEmpty) } @@ -503,8 +481,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureScreenImpression(event: expectedPixel) // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertTrue(sharedPixelHandlerMock.eventsFired.isEmpty) } @@ -517,8 +495,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureScreenImpression(event: expectedPixel) // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertTrue(sharedPixelHandlerMock.eventsFired.isEmpty) } @@ -530,7 +508,7 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureScreenImpression(.searchResults(.shown)) // THEN - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchResults(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) XCTAssertEqual(sharedPixelHandlerMock.receivedFlow, .duckAI) @@ -540,19 +518,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureEndOfJourneyDialogCTAActionIsCalledThenLegacyEndOfJourneyDismissedAndEndEngageSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.daxDialogsEndOfJourneyDismissed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureEndOfJourneyDialogCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.end(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -667,8 +643,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureDuckAIFireButtonCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) } func testWhenMeasureDuckAIFinalDialogImpressionThenLegacyFinalDialogShownUniquePixelFires() { @@ -679,8 +655,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureDuckAIFinalDialogImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) } func testWhenMeasureDuckAIFinalDialogCTAActionThenEndEngageSharedPixelFires() { @@ -708,8 +684,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureDuckAIQuerySelectionImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, .onboardingIntroDuckAIToggleImpressionUnique) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, Pixel.Event.onboardingIntroDuckAIToggleImpressionUnique.name) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchChatToggle(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) XCTAssertEqual(sharedPixelHandlerMock.receivedFlow, .duckAI) @@ -753,19 +729,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureTrySearchDialogNewTabDismissButtonTappedThenLegacyDismissTappedAndSearchDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingTrySearchDialogNewTabDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureTrySearchDialogNewTabDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.search(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -776,19 +750,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureTryVisitSiteDialogNewTabDismissButtonTappedThenLegacyDismissTappedAndVisitSiteDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingTryVisitSiteDialogNewTabDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureTryVisitSiteDialogNewTabDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.visitSite(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -799,19 +771,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureTryVisitSiteDialogDismissButtonTappedThenLegacyDismissTappedAndVisitSiteDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingTryVisitSiteDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureTryVisitSiteDialogDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.visitSite(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -822,19 +792,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureSearchResultDialogDismissButtonTappedThenLegacyDismissTappedAndSearchResultsDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingSearchResultDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureSearchResultDialogDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchResults(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -845,19 +813,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureTrackersDialogDismissButtonTappedThenLegacyDismissTappedAndTrackersBlockedDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingTrackersDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureTrackersDialogDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.trackersBlocked(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -868,19 +834,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureFireDialogDismissButtonTappedThenLegacyDismissTappedAndFireButtonDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingFireDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureFireDialogDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.fireButton(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -891,19 +855,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureEndOfJourneyDialogNewTabDismissButtonTappedThenLegacyDismissTappedAndEndDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingEndOfJourneyDialogNewTabDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureEndOfJourneyDialogNewTabDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.end(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -914,19 +876,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureEndOfJourneyDialogDismissButtonTappedThenLegacyDismissTappedAndEndDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingEndOfJourneyDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureEndOfJourneyDialogDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.end(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -937,19 +897,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureSubscriptionPromoDialogNewTabDismissButtonTappedThenLegacyDismissTappedAndSubscriptionPromoDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingSubscriptionDialogDismissButtonTapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureSubscriptionDialogNewTabDismissButtonTapped() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.subscriptionPromo(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -962,19 +920,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureChooseAppIconImpressionIsCalledThenLegacyChooseIconImpressionUniqueAndAppIconColorShownSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroChooseAppIconImpressionUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseAppIconImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.appIconColor(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -985,19 +941,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureChooseNonDefaultAppIconIsCalledThenLegacyChooseCustomIconColorPressedAndAppIconColorClickedSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroChooseCustomAppIconColorCTAPressed - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseAppIconColor(.green) // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.appIconColor(.clicked(.green))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1013,9 +967,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureChooseAppIconColor(.defaultAppIcon) // THEN - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.appIconColor(.clicked(.red))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1026,19 +979,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAddressBarPositionSelectionImpressionIsCalledThenLegacyChooseAddressBarImpressionUniqueAndAddressBarPositionShownSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroChooseAddressBarImpressionUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureAddressBarPositionSelectionImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addressBarPosition(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1049,19 +1000,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureChooseBottomAddressBarPositionIsCalledThenLegacyBottomAddressBarSelectedAndAddressBarBottomSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroBottomAddressBarSelected - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseAddressBarPosition(.bottom) // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addressBarPosition(.clicked(.bottom))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1077,9 +1026,8 @@ final class OnboardingPixelReporterTests: XCTestCase { sut.measureChooseAddressBarPosition(.top) // THEN - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addressBarPosition(.clicked(.top))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1092,19 +1040,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAddToDockPromoImpressionsIsCalledThenLegacyPromoImpressionsUniqueAndAddToDockShownSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingAddToDockPromoImpressionsUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureAddToDockPromoImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addToDock(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1115,19 +1061,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAddToDockPromoShowTutorialCTAActionIsCalledThenLegacyShowTutorialTappedAndAddToDockEngageSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingAddToDockPromoShowTutorialCTATapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureAddToDockPromoShowTutorialCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addToDock(.clicked(.engage))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1138,19 +1082,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAddToDockPromoDismissCTAActionThenLegacyPromoDismissTappedAndAddToDockDismissSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingAddToDockPromoDismissCTATapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureAddToDockPromoDismissCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.addToDock(.clicked(.dismiss))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1161,19 +1103,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureAddToDockTutorialDismissCTAActionIsCalledThenonboardingAddToDockTutorialDismissCTAPixelFires() { // GIVEN let expectedPixel = Pixel.Event.onboardingAddToDockTutorialDismissCTATapped - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureAddToDockTutorialDismissCTAAction() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertTrue(sharedPixelHandlerMock.eventsFired.isEmpty) } @@ -1182,19 +1122,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureSearchExperienceSelectionImpressionIsCalledThenLegacyChooseSearchExperienceImpressionUniqueAndSearchExperienceShownSharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroChooseSearchExperienceImpressionUnique - XCTAssertFalse(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertNil(OnboardingUniquePixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureSearchExperienceSelectionImpression() // THEN - XCTAssertTrue(OnboardingUniquePixelFireMock.didCallFire) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .legacyInitial }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .legacyInitial })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingUniquePixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchExperience(.shown)]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1205,19 +1143,17 @@ final class OnboardingPixelReporterTests: XCTestCase { func testWhenMeasureChooseAIChatIsCalledThenLegacyAIChatSelectedAndSearchExperienceSearchPlusDuckAISharedPixelsFire() { // GIVEN let expectedPixel = Pixel.Event.onboardingIntroAIChatSelected - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseAIChat() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchExperience(.clicked(.searchPlusDuckAI))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) @@ -1229,19 +1165,17 @@ final class OnboardingPixelReporterTests: XCTestCase { // GIVEN sharedPixelsStorageMock.onboardingVariant = nil let expectedPixel = Pixel.Event.onboardingIntroSearchOnlySelected - XCTAssertFalse(OnboardingPixelFireMock.didCallFire) - XCTAssertNil(OnboardingPixelFireMock.capturedPixelEvent) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, []) + XCTAssertFalse(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertNil(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, []) // WHEN sut.measureChooseSearchOnly() // THEN - XCTAssertTrue(OnboardingPixelFireMock.didCallFire) - XCTAssertEqual(OnboardingPixelFireMock.capturedPixelEvent, expectedPixel) + XCTAssertTrue(pixelKitMock.actualFireCalls.contains { $0.frequency == .standard }) + XCTAssertEqual(pixelKitMock.actualFireCalls.last(where: { $0.frequency == .standard })?.pixel.name, expectedPixel.name) XCTAssertEqual(expectedPixel.name, expectedPixel.name) - XCTAssertEqual(OnboardingPixelFireMock.capturedIncludeParameters, [.appVersion]) XCTAssertEqual(sharedPixelHandlerMock.eventsFired, [.searchExperience(.clicked(.searchOnly))]) XCTAssertEqual(sharedPixelHandlerMock.receivedSource, .duckAICustomProductPage) diff --git a/iOS/DuckDuckGoTests/PersistentPixelTests.swift b/iOS/DuckDuckGoTests/PersistentPixelTests.swift deleted file mode 100644 index 1385f67d3ec..00000000000 --- a/iOS/DuckDuckGoTests/PersistentPixelTests.swift +++ /dev/null @@ -1,436 +0,0 @@ -// -// PersistentPixelTests.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import FoundationExtensions -import XCTest -import Networking -@_spi(Testing) import Persistence -@testable import Core - -final class PersistentPixelTests: XCTestCase { - - var currentStorageURL: URL! - var persistentStorage: DefaultPersistentPixelStorage! - var timestampStorage: KeyValueStoring! - - var testDateString: String! - var oldDateString: String! - - override func setUp() { - super.setUp() - let (url, storage) = createPersistentStorage() - self.currentStorageURL = url - self.persistentStorage = storage - self.timestampStorage = MockKeyValueStore() - - PixelFiringMock.tearDown() - DelayedPixelFiringMock.tearDown() - - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - testDateString = formatter.string(from: Date()) - oldDateString = formatter.string(from: Date().addingTimeInterval(-.days(30))) - } - - override func tearDown() { - super.tearDown() - try? FileManager.default.removeItem(at: currentStorageURL) - - PixelFiringMock.tearDown() - DelayedPixelFiringMock.tearDown() - } - - func testWhenDailyAndCountPixelsSendSuccessfully_ThenNoPixelsAreStored() throws { - let persistentPixel = createPersistentPixel() - let expectation = expectation(description: "fireDailyAndCount") - - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionMemoryWarning, - withAdditionalParameters: ["key": "value"], - includedParameters: [.appVersion], - completion: { errors in - expectation.fulfill() - XCTAssertNil(errors.dailyPixelStorageError) - XCTAssertNil(errors.countPixelStorageError) - } - ) - - wait(for: [expectation], timeout: 1.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixels, []) - - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.pixelName, Pixel.Event.networkProtectionMemoryWarning.name) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.params, ["key": "value", PixelParameters.originalPixelTimestamp: testDateString]) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.includedParams, [.appVersion]) - } - - func testWhenDailyPixelFailsDueToAlreadySentError_ThenNoPixelIsStored() throws { - PixelFiringMock.expectedDailyPixelFireError = DailyPixel.Error.alreadyFired // This is expected behaviour from the daily pixel - - let persistentPixel = createPersistentPixel() - let error = NSError(domain: "domain", code: 1) - let expectation = expectation(description: "fireDailyAndCount") - - persistentPixel.fireDailyAndCount( - pixel: .appLaunch, - error: error, - withAdditionalParameters: ["param": "value"], - includedParameters: [.appVersion], - completion: { errors in - expectation.fulfill() - XCTAssertNil(errors.dailyPixelStorageError) - XCTAssertNil(errors.countPixelStorageError) - } - ) - - wait(for: [expectation], timeout: 1.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssert(storedPixels.isEmpty) - } - - func testWhenDailyAndCountPixelsFail_ThenPixelsAreStored() throws { - PixelFiringMock.expectedDailyPixelFireError = NSError(domain: "PixelFailure", code: 1) - PixelFiringMock.expectedCountPixelFireError = NSError(domain: "PixelFailure", code: 2) - - let persistentPixel = createPersistentPixel() - let error = NSError(domain: "domain", code: 1) - let expectation = expectation(description: "fireDailyAndCount") - - persistentPixel.fireDailyAndCount( - pixel: .appLaunch, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: ["param": "value"], - includedParameters: [.appVersion], - completion: { errors in - expectation.fulfill() - XCTAssertNil(errors.dailyPixelStorageError) - XCTAssertNil(errors.countPixelStorageError) - } - ) - - wait(for: [expectation], timeout: 1.0) - - let storedPixels = try persistentStorage.storedPixels() - let expectedParams = [ - "param": "value", - PixelParameters.originalPixelTimestamp: testDateString, - PixelParameters.errorDomain: error.domain, - PixelParameters.errorCode: "\(error.code)" - ] - - XCTAssertEqual(storedPixels.count, 2) - XCTAssert(storedPixels.contains { - $0.eventName == Pixel.Event.appLaunch.name + DailyPixel.Constant.legacyDailyPixelSuffixes.countSuffix && - $0.additionalParameters == expectedParams - }) - - XCTAssert(storedPixels.contains { - $0.eventName == Pixel.Event.appLaunch.name + DailyPixel.Constant.legacyDailyPixelSuffixes.dailySuffix && - $0.additionalParameters == expectedParams - }) - - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.pixelName, Pixel.Event.appLaunch.name) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.params, ["param": "value", PixelParameters.originalPixelTimestamp: testDateString]) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.includedParams, [.appVersion]) - } - - func testWhenOnlyCountPixelFails_ThenCountPixelIsStored() throws { - PixelFiringMock.expectedCountPixelFireError = NSError(domain: "PixelFailure", code: 1) - - let persistentPixel = createPersistentPixel() - let expectation = expectation(description: "fireDailyAndCount") - - persistentPixel.fireDailyAndCount( - pixel: .appLaunch, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - withAdditionalParameters: ["param": "value"], - includedParameters: [.appVersion], - completion: { errors in - expectation.fulfill() - XCTAssertNil(errors.dailyPixelStorageError) - XCTAssertNil(errors.countPixelStorageError) - } - ) - - wait(for: [expectation], timeout: 1.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixels.count, 1) - XCTAssert(storedPixels.contains { - $0.eventName == Pixel.Event.appLaunch.name + DailyPixel.Constant.legacyDailyPixelSuffixes.countSuffix && - $0.additionalParameters == ["param": "value", PixelParameters.originalPixelTimestamp: testDateString] - }) - - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.pixelName, Pixel.Event.appLaunch.name) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.params, ["param": "value", PixelParameters.originalPixelTimestamp: testDateString]) - XCTAssertEqual(PixelFiringMock.lastDailyPixelInfo?.includedParams, [.appVersion]) - } - - func testWhenPixelsAreStored_AndSendQueuedPixelsIsCalled_AndPixelRetrySucceeds_ThenPixelsAreRemovedFromStorage() throws { - let persistentPixel = createPersistentPixel() - let expectation = expectation(description: "sendQueuedPixels") - - let params = ["key": "value", PixelParameters.originalPixelTimestamp: testDateString!] - let pixel = PersistentPixelMetadata(eventName: "test1", additionalParameters: params, includedParameters: [.appVersion]) - let pixel2 = PersistentPixelMetadata(eventName: "test2", additionalParameters: params, includedParameters: [.appVersion]) - let pixel3 = PersistentPixelMetadata(eventName: "test3", additionalParameters: params, includedParameters: [.appVersion]) - let pixel4 = PersistentPixelMetadata(eventName: "test4", additionalParameters: params, includedParameters: [.appVersion]) - - try persistentStorage.append(pixels: [pixel, pixel2, pixel3, pixel4]) - persistentPixel.sendQueuedPixels { _ in - expectation.fulfill() - } - - wait(for: [expectation], timeout: 3.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssert(storedPixels.isEmpty) - } - - func testWhenPixelIsStored_AndSendQueuedPixelsIsCalled_ThenPixelIsSent() throws { - let persistentPixel = createPersistentPixel() - let expectation = expectation(description: "sendQueuedPixels") - - let pixel = PersistentPixelMetadata( - eventName: "test", - additionalParameters: ["key": "value", PixelParameters.originalPixelTimestamp: testDateString], - includedParameters: [.appVersion] - ) - - try persistentStorage.append(pixels: [pixel]) - persistentPixel.sendQueuedPixels { _ in - expectation.fulfill() - } - - wait(for: [expectation], timeout: 3.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssert(storedPixels.isEmpty) - - XCTAssertEqual(PixelFiringMock.lastPixelName, "test") - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.params, [ - "key": "value", - PixelParameters.retriedPixel: "1", - PixelParameters.originalPixelTimestamp: testDateString - ]) - XCTAssertEqual(PixelFiringMock.lastPixelInfo?.includedParams, [.appVersion]) - } - - func testWhenPixelIsStored_AndSendQueuedPixelsIsCalled_AndPixelIsOlderThan28Days_ThenPixelIsNotSent_AndPixelIsNoLongerStored() throws { - let persistentPixel = createPersistentPixel() - let expectation = expectation(description: "sendQueuedPixels") - - let pixel = PersistentPixelMetadata( - eventName: "test", - additionalParameters: ["key": "value", PixelParameters.originalPixelTimestamp: oldDateString], - includedParameters: [.appVersion] - ) - - try persistentStorage.append(pixels: [pixel]) - persistentPixel.sendQueuedPixels { _ in - expectation.fulfill() - } - - wait(for: [expectation], timeout: 3.0) - - let storedPixels = try persistentStorage.storedPixels() - XCTAssert(storedPixels.isEmpty) - - XCTAssertNil(PixelFiringMock.lastPixelName) - XCTAssertNil(PixelFiringMock.lastDailyPixelInfo) - } - - func testWhenPixelQueueIsProcessing_AndNewFailedPixelIsReceived_ThenPixelIsStoredEvenIfProcessingIsActive() throws { - PixelFiringMock.expectedCountPixelFireError = NSError(domain: "PixelFailure", code: 1) - - let persistentPixel = createPersistentPixel(pixelFiring: DelayedPixelFiringMock.self) - let sendQueuedPixelsExpectation = expectation(description: "sendQueuedPixels") - - let initialPixel = PersistentPixelMetadata( - eventName: "test", - additionalParameters: [PixelParameters.originalPixelTimestamp: testDateString], - includedParameters: [.appVersion] - ) - - try persistentStorage.append(pixels: [initialPixel]) - - // Wait for the queued pixel completion handlers to be received by the mock: - let delayedPixelPendingClosureExpectation = expectation(description: "completionHandlerUpdateClosure") - DelayedPixelFiringMock.completionHandlerUpdateClosure = { count in - if count == 1 { - delayedPixelPendingClosureExpectation.fulfill() - } - } - - // Initiate pixel queue processing: - persistentPixel.sendQueuedPixels { _ in - sendQueuedPixelsExpectation.fulfill() - } - - wait(for: [delayedPixelPendingClosureExpectation], timeout: 3.0) - - // Trigger a failed pixel call while processing, and wait for it to complete: - let dailyCountPixelExpectation = expectation(description: "sendQueuedPixels") - persistentPixel.fireDailyAndCount(pixel: .appLaunch, withAdditionalParameters: [:], includedParameters: [.appVersion], completion: { _ in - dailyCountPixelExpectation.fulfill() - }) - wait(for: [dailyCountPixelExpectation], timeout: 3.0) - - // Check that the new failed pixel call caused a pixel to get stored: - let storedPixelsWhenSendingQueuedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixelsWhenSendingQueuedPixels.count, 2) - XCTAssert(storedPixelsWhenSendingQueuedPixels.contains(initialPixel)) - - // Complete pixel processing callback: - DelayedPixelFiringMock.callCompletionHandler() - - wait(for: [sendQueuedPixelsExpectation], timeout: 3.0) - - let storedPixelsAfterSendingQueuedPixels = try persistentStorage.storedPixels() - - XCTAssertEqual(storedPixelsAfterSendingQueuedPixels.count, 1) - XCTAssert(storedPixelsAfterSendingQueuedPixels.contains(where: { pixel in - return pixel.eventName == Pixel.Event.appLaunch.name + DailyPixel.Constant.dailyPixelSuffixes.countSuffix - && pixel.additionalParameters == [PixelParameters.originalPixelTimestamp: testDateString] - && pixel.includedParameters == [.appVersion] - })) - } - - func testWhenPixelQueueIsRetrying_AndNewFailedPixelIsReceived_AndRetryingFails_ThenExistingAndNewPixelsAreStored() throws { - PixelFiringMock.expectedCountPixelFireError = NSError(domain: "PixelFailure", code: 1) - DelayedPixelFiringMock.completionError = NSError(domain: "PixelFailure", code: 1) - - let persistentPixel = createPersistentPixel(pixelFiring: DelayedPixelFiringMock.self) - let initialPixel = PersistentPixelMetadata( - eventName: "test", - additionalParameters: [PixelParameters.originalPixelTimestamp: testDateString], - includedParameters: [.appVersion] - ) - - try persistentStorage.append(pixels: [initialPixel]) - - // Wait for the queued pixel completion handlers to be received by the mock: - let delayedPixelPendingClosureExpectation = expectation(description: "completionHandlerUpdateClosure") - DelayedPixelFiringMock.completionHandlerUpdateClosure = { count in - if count == 1 { - delayedPixelPendingClosureExpectation.fulfill() - } - } - - // Initiate pixel queue processing: - let sendQueuedPixelsExpectation = expectation(description: "sendQueuedPixels") - persistentPixel.sendQueuedPixels { _ in - sendQueuedPixelsExpectation.fulfill() - } - - wait(for: [delayedPixelPendingClosureExpectation], timeout: 3.0) - - // Trigger a failed pixel call while processing, and wait for it to complete: - let dailyCountPixelExpectation = expectation(description: "daily/count pixel call") - persistentPixel.fireDailyAndCount(pixel: .appLaunch, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - withAdditionalParameters: [:], - includedParameters: [.appVersion], - completion: { _ in - dailyCountPixelExpectation.fulfill() - }) - wait(for: [dailyCountPixelExpectation], timeout: 3.0) - - // Check that the new failed pixel call caused a pixel to get stored: - let storedPixelsWhenSendingQueuedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixelsWhenSendingQueuedPixels.count, 2) - XCTAssert(storedPixelsWhenSendingQueuedPixels.contains(initialPixel)) - - // Complete pixel processing callback: - DelayedPixelFiringMock.callCompletionHandler() - - wait(for: [sendQueuedPixelsExpectation], timeout: 3.0) - - let storedPixelsAfterSendingQueuedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixelsAfterSendingQueuedPixels.count, 2) - XCTAssert(storedPixelsAfterSendingQueuedPixels.contains(initialPixel)) - XCTAssert(storedPixelsAfterSendingQueuedPixels.contains(where: { pixel in - return pixel.eventName == Pixel.Event.appLaunch.name + DailyPixel.Constant.legacyDailyPixelSuffixes.countSuffix - && pixel.additionalParameters == [PixelParameters.originalPixelTimestamp: testDateString] - && pixel.includedParameters == [.appVersion] - })) - } - - func testWhenPixelQueueHasRecentlyProcessed_ThenPixelsAreNotProcessed() throws { - let currentDate = Date() - let persistentPixel = createPersistentPixel(dateGenerator: { currentDate }) - let sendQueuedPixelsExpectation = expectation(description: "sendQueuedPixels") - - let pixel = PersistentPixelMetadata( - eventName: "unfired_pixel", - additionalParameters: [PixelParameters.originalPixelTimestamp: testDateString], - includedParameters: [.appVersion] - ) - - try persistentStorage.append(pixels: [pixel]) - - // Set a last processing date of 1 minute ago: - timestampStorage.set(currentDate.addingTimeInterval(-60), forKey: PersistentPixel.Constants.lastProcessingDateKey) - - persistentPixel.sendQueuedPixels { _ in - sendQueuedPixelsExpectation.fulfill() - } - - wait(for: [sendQueuedPixelsExpectation], timeout: 3.0) - - let storedPixelsAfterSendingQueuedPixels = try persistentStorage.storedPixels() - XCTAssertEqual(storedPixelsAfterSendingQueuedPixels, [pixel]) - XCTAssertNil(PixelFiringMock.lastPixelName) - } - - // MARK: - Test Utilities - - private func createPersistentPixel(pixelFiring: PixelFiring.Type = PixelFiringMock.self, - dailyPixelFiring: DailyPixelFiring.Type = PixelFiringMock.self, - dateGenerator: (() -> Date)? = nil) -> PersistentPixel { - return PersistentPixel( - pixelFiring: pixelFiring, - dailyPixelFiring: dailyPixelFiring, - persistentPixelStorage: persistentStorage, - lastProcessingDateStorage: timestampStorage, - dateGenerator: dateGenerator ?? self.dateGenerator - ) - } - - private func createPersistentStorage() -> (URL, DefaultPersistentPixelStorage) { - let storageDirectory = FileManager.default.temporaryDirectory - let fileName = UUID().uuidString.appendingPathExtension("json") - - return ( - storageDirectory.appendingPathComponent(fileName), - DefaultPersistentPixelStorage(fileName: fileName, storageDirectory: storageDirectory) - ) - } - - private func dateGenerator() -> Date { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter.date(from: testDateString)! - } - -} diff --git a/iOS/DuckDuckGoTests/PixelEventPixelKitTests.swift b/iOS/DuckDuckGoTests/PixelEventPixelKitTests.swift index d1926a49c00..b517ff12549 100644 --- a/iOS/DuckDuckGoTests/PixelEventPixelKitTests.swift +++ b/iOS/DuckDuckGoTests/PixelEventPixelKitTests.swift @@ -156,4 +156,56 @@ final class PixelEventPixelKitTests: XCTestCase { XCTAssertNil(event.parameters) XCTAssertNil(event.error) } + + // MARK: - Pixel name spot checks + + func testUnifiedToggleInputPixelEventNames() { + let expectedNames: [(Pixel.Event, String)] = [ + (.unifiedToggleInputImageGenerationSelected, "m_aichat_unified_input_image_generation_selected"), + (.unifiedToggleInputImageGenerationDeselected, "m_aichat_unified_input_image_generation_deselected"), + (.unifiedToggleInputImageGenerationSubmitted, "m_aichat_unified_input_image_generation_submitted"), + (.unifiedToggleInputWebSearchSelected, "m_aichat_unified_input_web_search_selected"), + (.unifiedToggleInputWebSearchDeselected, "m_aichat_unified_input_web_search_deselected"), + (.unifiedToggleInputWebSearchSubmitted, "m_aichat_unified_input_web_search_submitted"), + (.unifiedToggleInputModelSelected, "m_aichat_unified_input_model_selected"), + (.unifiedToggleInputModelPickerShown, "m_aichat_unified_input_model_picker_shown"), + (.unifiedToggleInputReasoningEffortSelected, "m_aichat_unified_input_reasoning_effort_selected"), + (.unifiedToggleInputReasoningEffortPickerShown, "m_aichat_unified_input_reasoning_effort_picker_shown"), + (.unifiedToggleInputImageAttached, "m_aichat_unified_input_image_attached"), + (.unifiedToggleInputImageRemoved, "m_aichat_unified_input_image_removed"), + (.unifiedToggleInputFileAttached, "m_aichat_unified_input_file_attached"), + (.unifiedToggleInputFileRemoved, "m_aichat_unified_input_file_removed"), + (.unifiedToggleInputFileValidationFailed, "m_aichat_unified_input_file_validation_failed"), + (.unifiedToggleInputVoiceTapped, "m_aichat_unified_input_voice_tapped"), + (.unifiedToggleInputStopGenerationTapped, "m_aichat_unified_input_stop_generation_tapped"), + (.unifiedToggleInputSubscriptionUpsellTriggered, "m_aichat_unified_input_subscription_upsell_triggered"), + (.unifiedToggleInputChatHeaderUpgradeTapped, "m_aichat_unified_input_chat_header_upgrade_tapped"), + (.unifiedToggleInputChatHeaderUpgradeShown, "m_aichat_unified_input_chat_header_upgrade_shown"), + (.unifiedToggleInputPromptSubmitted, "m_aichat_unified_input_prompt_submitted"), + (.unifiedToggleInputShowModelPicker, "aichat_unified_input_show_model_picker"), + (.unifiedToggleInputSubmitChangeModel, "aichat_unified_input_submit_change_model"), + (.unifiedToggleInputSubmitChangeModelPromptSent, "aichat_unified_input_submit_change_model_prompt_sent") + ] + + for (event, expectedName) in expectedNames { + XCTAssertEqual(event.name, expectedName) + } + } + + func testDuckAIAutocompletePixelEventNames() { + let expectedNames: [(Pixel.Event, String)] = [ + (.autocompleteDuckAIClickWebsite, "m_autocomplete_duckai_click_website"), + (.autocompleteDuckAIClickBookmark, "m_autocomplete_duckai_click_bookmark"), + (.autocompleteDuckAIClickFavorite, "m_autocomplete_duckai_click_favorite"), + (.autocompleteDuckAIClickHistorySearch, "m_autocomplete_duckai_click_history_search"), + (.autocompleteDuckAIClickHistorySite, "m_autocomplete_duckai_click_history_site"), + (.autocompleteDuckAIClickSwitchToTab, "m_autocomplete_duckai_click_switch_to_tab"), + (.autocompleteDuckAIClickChatHistory, "m_autocomplete_duckai_click_chat_history"), + (.autocompleteDuckAIClickSearchDuckDuckGo, "m_autocomplete_duckai_click_search_duckduckgo") + ] + + for (event, expectedName) in expectedNames { + XCTAssertEqual(event.name, expectedName) + } + } } diff --git a/iOS/DuckDuckGoTests/PixelTests.swift b/iOS/DuckDuckGoTests/PixelTests.swift index 4083bc4dcb7..d2cebfbdf8d 100644 --- a/iOS/DuckDuckGoTests/PixelTests.swift +++ b/iOS/DuckDuckGoTests/PixelTests.swift @@ -18,299 +18,15 @@ // import XCTest -import OHHTTPStubs -import OHHTTPStubsSwift -import Networking @testable import Core -import Common -import FoundationExtensions import PixelKit -class PixelTests: XCTestCase { - - let host = "improving.duckduckgo.com" - let testAgent = "Test Agent" - let userAgentName = "User-Agent" - - override func setUpWithError() throws { - try super.setUpWithError() - - Pixel.isDryRun = false - } - - override func tearDown() { - Pixel.isDryRun = true - - HTTPStubs.removeAllStubs() - super.tearDown() - } - - func testUnifiedToggleInputPixelEventNames() { - let expectedNames: [(Pixel.Event, String)] = [ - (.unifiedToggleInputImageGenerationSelected, "m_aichat_unified_input_image_generation_selected"), - (.unifiedToggleInputImageGenerationDeselected, "m_aichat_unified_input_image_generation_deselected"), - (.unifiedToggleInputImageGenerationSubmitted, "m_aichat_unified_input_image_generation_submitted"), - (.unifiedToggleInputWebSearchSelected, "m_aichat_unified_input_web_search_selected"), - (.unifiedToggleInputWebSearchDeselected, "m_aichat_unified_input_web_search_deselected"), - (.unifiedToggleInputWebSearchSubmitted, "m_aichat_unified_input_web_search_submitted"), - (.unifiedToggleInputModelSelected, "m_aichat_unified_input_model_selected"), - (.unifiedToggleInputModelPickerShown, "m_aichat_unified_input_model_picker_shown"), - (.unifiedToggleInputReasoningEffortSelected, "m_aichat_unified_input_reasoning_effort_selected"), - (.unifiedToggleInputReasoningEffortPickerShown, "m_aichat_unified_input_reasoning_effort_picker_shown"), - (.unifiedToggleInputImageAttached, "m_aichat_unified_input_image_attached"), - (.unifiedToggleInputImageRemoved, "m_aichat_unified_input_image_removed"), - (.unifiedToggleInputFileAttached, "m_aichat_unified_input_file_attached"), - (.unifiedToggleInputFileRemoved, "m_aichat_unified_input_file_removed"), - (.unifiedToggleInputFileValidationFailed, "m_aichat_unified_input_file_validation_failed"), - (.unifiedToggleInputVoiceTapped, "m_aichat_unified_input_voice_tapped"), - (.unifiedToggleInputStopGenerationTapped, "m_aichat_unified_input_stop_generation_tapped"), - (.unifiedToggleInputSubscriptionUpsellTriggered, "m_aichat_unified_input_subscription_upsell_triggered"), - (.unifiedToggleInputChatHeaderUpgradeTapped, "m_aichat_unified_input_chat_header_upgrade_tapped"), - (.unifiedToggleInputChatHeaderUpgradeShown, "m_aichat_unified_input_chat_header_upgrade_shown"), - (.unifiedToggleInputPromptSubmitted, "m_aichat_unified_input_prompt_submitted"), - (.unifiedToggleInputShowModelPicker, "aichat_unified_input_show_model_picker"), - (.unifiedToggleInputSubmitChangeModel, "aichat_unified_input_submit_change_model"), - (.unifiedToggleInputSubmitChangeModelPromptSent, "aichat_unified_input_submit_change_model_prompt_sent") - ] - - for (event, expectedName) in expectedNames { - XCTAssertEqual(event.name, expectedName) - } - } - - func testDuckAIAutocompletePixelEventNames() { - let expectedNames: [(Pixel.Event, String)] = [ - (.autocompleteDuckAIClickWebsite, "m_autocomplete_duckai_click_website"), - (.autocompleteDuckAIClickBookmark, "m_autocomplete_duckai_click_bookmark"), - (.autocompleteDuckAIClickFavorite, "m_autocomplete_duckai_click_favorite"), - (.autocompleteDuckAIClickHistorySearch, "m_autocomplete_duckai_click_history_search"), - (.autocompleteDuckAIClickHistorySite, "m_autocomplete_duckai_click_history_site"), - (.autocompleteDuckAIClickSwitchToTab, "m_autocomplete_duckai_click_switch_to_tab"), - (.autocompleteDuckAIClickChatHistory, "m_autocomplete_duckai_click_chat_history"), - (.autocompleteDuckAIClickSearchDuckDuckGo, "m_autocomplete_duckai_click_search_duckduckgo") - ] - - for (event, expectedName) in expectedNames { - XCTAssertEqual(event.name, expectedName) - } - } - - func testWhenPixelFiredAndOverridesAppVersionThenOverrideIsUsed() { - let expectation = XCTestExpectation() - - stub(condition: containsQueryParams([PixelParameters.appVersion: "override"])) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, withAdditionalParameters: [PixelParameters.appVersion: "override"]) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenPixelFiredAndIncludedParametersIsEmptyThenAppVersionIsNotSet() { - let expectation = XCTestExpectation() - - stub(condition: { request in - return request.url?.queryParameters()?[PixelParameters.appVersion] == nil - }) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, includedParameters: []) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenPixelFiredThenAppVersionIsSetByDefault() { - let expectation = XCTestExpectation() - - stub(condition: containsQueryParams([PixelParameters.appVersion: AppVersion.shared.versionNumber])) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenPixelFiredThenAPIHeadersAreAdded() { - let expectation = XCTestExpectation() - - stub(condition: hasHeaderNamed(userAgentName, value: testAgent)) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - let headers = APIRequest.Headers(userAgent: testAgent) - Pixel.fire(pixel: .appLaunch, forDeviceType: .phone, withHeaders: headers) - - wait(for: [expectation], timeout: 5.0) - - } - - func testWhenPixelIsFiredWithAdditionalParametersThenParametersAdded() { - let expectation = XCTestExpectation() - let params = ["param1": "value1", "param2": "value2"] - - stub(condition: isHost(host) && isPath("/t/ml_ios_phone")) { request -> HTTPStubsResponse in - XCTAssertEqual("value1", request.url?.getParameter(named: "param1")) - XCTAssertEqual("value2", request.url?.getParameter(named: "param2")) - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .phone, withAdditionalParameters: params) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenAppLaunchPixelIsFiredFromPhoneThenCorrectURLRequestIsMade() { - let expectation = XCTestExpectation() - - stub(condition: isHost(host) && isPath("/t/ml_ios_phone")) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .phone) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenAppLaunchPixelIsFiredFromTabletThenCorrectURLRequestIsMade() { - let expectation = XCTestExpectation() - - stub(condition: isHost(host) && isPath("/t/ml_ios_tablet")) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .pad) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenAppLaunchPixelIsFiredFromUnspecifiedThenCorrectURLRequestIsMadeAsPhone() { - let expectation = XCTestExpectation() - - stub(condition: isHost(host) && isPath("/t/ml_ios_phone")) { _ -> HTTPStubsResponse in - expectation.fulfill() - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .unspecified) - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenPixelFiresSuccessfullyThenCompletesWithNoError() { - let expectation = XCTestExpectation() - - stub(condition: isHost(host)) { _ -> HTTPStubsResponse in - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .phone) { error in - XCTAssertNil(error) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - func testWhenPixelFiresUnsuccessfullyThenCompletesWithError() { - let expectation = XCTestExpectation() - - stub(condition: isHost(host)) { _ -> HTTPStubsResponse in - return HTTPStubsResponse(data: Data(), statusCode: 404, headers: nil) - } - - Pixel.fire(pixel: .appLaunch, forDeviceType: .phone) { error in - XCTAssertNotNil(error) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - func testPixelDebouncePreventsFiringWithinInterval() throws { - throw XCTSkip("Flaky") - let firstFireExpectation = XCTestExpectation(description: "First pixel fire should succeed") - let thirdFireExpectation = XCTestExpectation(description: "Third pixel fire should succeed after debounce interval") - - stub(condition: isHost(self.host)) { _ -> HTTPStubsResponse in - return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) - } - - let pixel = Pixel.Event.appLaunch - let debounceInterval = 1 // Debounce interval of 5 seconds - - // Should be OK - Pixel.fire(pixel: pixel, forDeviceType: .phone, onComplete: { error in - XCTAssertNil(error) - firstFireExpectation.fulfill() - }, debounce: debounceInterval) - - // Should be debounced - Pixel.fire(pixel: pixel, forDeviceType: .phone, onComplete: { _ in - }, debounce: debounceInterval) - - // Should be OK - DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.seconds(debounceInterval + 1)) { - Pixel.fire(pixel: pixel, forDeviceType: .phone, onComplete: { error in - XCTAssertNil(error) - thirdFireExpectation.fulfill() - }, debounce: debounceInterval) - } - - wait(for: [firstFireExpectation, thirdFireExpectation], timeout: Double(debounceInterval + 4)) - } - - func testWhenDefiningUnderlyingErrorParametersThenNestedErrorsAreIncluded() { - let underlyingError4 = NSError(domain: "underlyingError4", code: 5, userInfo: [:]) - let underlyingError3 = NSError(domain: "underlyingError3", code: 4, userInfo: [NSUnderlyingErrorKey: underlyingError4]) - let underlyingError2 = NSError(domain: "underlyingError2", code: 3, userInfo: [NSUnderlyingErrorKey: underlyingError3]) - let underlyingError1 = NSError(domain: "underlyingError1", code: 2, userInfo: [NSUnderlyingErrorKey: underlyingError2]) - let error = NSError(domain: "error", code: 1, userInfo: [NSUnderlyingErrorKey: underlyingError1]) - - var parameters: [String: String] = [:] - parameters.appendErrorPixelParams(error: error) - - XCTAssertEqual(parameters.count, 10) - XCTAssertEqual(parameters["d"], error.domain) - XCTAssertEqual(parameters["e"], String(error.code)) - XCTAssertEqual(parameters["ud"], underlyingError1.domain) - XCTAssertEqual(parameters["ue"], String(underlyingError1.code)) - XCTAssertEqual(parameters["ud2"], underlyingError2.domain) - XCTAssertEqual(parameters["ue2"], String(underlyingError2.code)) - XCTAssertEqual(parameters["ud3"], underlyingError3.domain) - XCTAssertEqual(parameters["ue3"], String(underlyingError3.code)) - XCTAssertEqual(parameters["ud4"], underlyingError4.domain) - XCTAssertEqual(parameters["ue4"], String(underlyingError4.code)) - } - - func testWhenDefiningUnderlyingErrorParametersAndThereIsNoUnderlyingErrorThenOnlyTopLevelParametersAreIncluded() { - let error = NSError(domain: "error", code: 1, userInfo: [:]) - - var parameters: [String: String] = [:] - parameters.appendErrorPixelParams(error: error) - - XCTAssertEqual(parameters.count, 2) - XCTAssertEqual(parameters["d"], error.domain) - XCTAssertEqual(parameters["e"], String(error.code)) - } - -} - -/// Pins PixelKit's default naming on iOS to what the legacy `Pixel` produces, so a new pixel written +/// Pins PixelKit's default naming on iOS to what the legacy `Pixel` produced, so a pixel written /// against PixelKit lands on the wire with the same shape as the pixels around it. final class PixelKitLegacyNamingParityTests: XCTestCase { - /// The legacy wire name: `Pixel.fire(pixelNamed:)` appends `_ios_` while building the - /// URL, after `DailyPixel` has already appended the frequency suffix to the name. + /// The legacy wire name: `Pixel.fire(pixelNamed:)` appended `_ios_` while building the + /// URL, after `DailyPixel` had already appended the frequency suffix to the name. private func legacyWireName(_ name: String, formFactor: String) -> String { URL.makePixelURL(pixelName: name, formFactor: formFactor, includeATB: false) .lastPathComponent @@ -363,7 +79,7 @@ final class PixelKitLegacyNamingParityTests: XCTestCase { legacyWireName("m_example_count", formFactor: "tablet")]) } - /// The legacy system adds no prefix of its own; names carry their own `m_`. PixelKit's default + /// The legacy system added no prefix of its own; names carry their own `m_`. PixelKit's default /// must not add one either. func testDefaultPixelAddsNoPrefixOfItsOwn() { let names = pixelKitNames("mf_bp", frequency: .standard, source: .iOS) diff --git a/iOS/DuckDuckGoTests/UnifiedToggleInput/UnifiedToggleInputCoordinatorTests.swift b/iOS/DuckDuckGoTests/UnifiedToggleInput/UnifiedToggleInputCoordinatorTests.swift index 914bf13109e..1d2fc1a49c8 100644 --- a/iOS/DuckDuckGoTests/UnifiedToggleInput/UnifiedToggleInputCoordinatorTests.swift +++ b/iOS/DuckDuckGoTests/UnifiedToggleInput/UnifiedToggleInputCoordinatorTests.swift @@ -403,9 +403,6 @@ final class UnifiedToggleInputCoordinatorTests: XCTestCase { // MARK: - Recovery Picker Session Pixels func test_recoveryPickerSession_fullFunnel_smokeTest() { - let previousDryRun = Pixel.isDryRun - Pixel.isDryRun = true - defer { Pixel.isDryRun = previousDryRun } _ = sut.prepareExternalPromptSubmission() let userScript = makeBridgeReadyUserScript() @@ -420,9 +417,6 @@ final class UnifiedToggleInputCoordinatorTests: XCTestCase { } func test_recoveryPickerSession_submitChangeModelPixel_smokeTest_withoutRecoveryPin() { - let previousDryRun = Pixel.isDryRun - Pixel.isDryRun = true - defer { Pixel.isDryRun = previousDryRun } _ = sut.prepareExternalPromptSubmission() let userScript = makeBridgeReadyUserScript() @@ -437,9 +431,6 @@ final class UnifiedToggleInputCoordinatorTests: XCTestCase { } func test_recoveryPickerSession_promptSentPixel_notFiredWithoutRecoveryPin() { - let previousDryRun = Pixel.isDryRun - Pixel.isDryRun = true - defer { Pixel.isDryRun = previousDryRun } sut.modelStore.models = [makeModel(id: "gpt-5", access: true)] sut.unifiedToggleInputVC(sut.viewController, didSubmitText: "first prompt", mode: .aiChat) diff --git a/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 2f2e59c6331..5fc3d0a1ad4 100644 --- a/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -39,7 +39,6 @@ import PrivacyConfig final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { - private static let persistentPixel: PersistentPixelFiring = PersistentPixel() private var cancellables = Set() private let subscriptionManager: (any SubscriptionManager)? private let configurationStore = ConfigurationStore() @@ -53,13 +52,11 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { switch event { case .userBecameActive: + // PixelKit drains its own retry queue after any successful fire, so this one alone + // covers what a manual sendQueuedPixels() call used to do. PixelKit.fire(Pixel.Event.networkProtectionActiveUser, frequency: .legacyDailyNoSuffix, options: .parameters([PixelParameters.vpnCohort: PixelKit.cohort(from: defaults.vpnFirstEnabled)])) - - persistentPixel.sendQueuedPixels { error in - Logger.networkProtection.error("Failed to send queued pixels, with error: \(error)") - } case .connectionTesterStatusChange(let status, let server): switch status { case .failed(let duration): @@ -179,26 +176,11 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { switch step { case .begin: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionRekeyAttempt, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionRekeyAttempt, frequency: .legacyDailyAndCount, options: .withRetry) case .failure(let error): - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionRekeyFailure, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionRekeyFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry) case .success: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionRekeyCompleted, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionRekeyCompleted, frequency: .legacyDailyAndCount, options: .withRetry) } case .tunnelStartAttempt(let step): switch step { @@ -213,27 +195,12 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { switch step { case .begin: if loopDetector.connectionLoopDetected { return } - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelStartAttempt, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelStartAttempt, frequency: .legacyDailyAndCount, options: .withRetry) case .failure(let error): if loopDetector.connectionLoopDetected { return } - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelStartFailure, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelStartFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry) case .success: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelStartSuccess, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelStartSuccess, frequency: .legacyDailyAndCount, options: .withRetry) } case .tunnelStopAttempt(let step): switch step { @@ -267,26 +234,11 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { switch step { case .begin: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelUpdateAttempt, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateAttempt, frequency: .legacyDailyAndCount, options: .withRetry) case .failure(let error): - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelUpdateFailure, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry) case .success: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionTunnelUpdateSuccess, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionTunnelUpdateSuccess, frequency: .legacyDailyAndCount, options: .withRetry) } case .tunnelWakeAttempt(let step): switch step { @@ -345,26 +297,11 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { switch step { case .begin: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionServerMigrationAttempt, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttempt, frequency: .legacyDailyAndCount, options: .withRetry) case .failure(let error): - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionServerMigrationAttemptFailure, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttemptFailure.withError(error), frequency: .legacyDailyAndCount, options: .withRetry) case .success: - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionServerMigrationAttemptSuccess, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: nil, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionServerMigrationAttemptSuccess, frequency: .legacyDailyAndCount, options: .withRetry) } case .tunnelStartOnDemandWithoutAccessToken(let error): Logger.networkProtection.error("🔴 Starting tunnel without an auth token") @@ -378,12 +315,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { case .adapterEndTemporaryShutdownStateRecoveryFailure(let error): PixelKit.fire(Pixel.Event.networkProtectionAdapterEndTemporaryShutdownStateRecoveryFailure.withError(error), frequency: .dailyAndCount) case .connectionFailureLoopDetected(let error): - persistentPixel.fireDailyAndCount( - pixel: .networkProtectionConnectionFailureLoopDetected, - pixelNameSuffixes: DailyPixel.Constant.legacyDailyPixelSuffixes, - error: error, - withAdditionalParameters: [:], - includedParameters: [.appVersion]) { _ in } + PixelKit.fire(Pixel.Event.networkProtectionConnectionFailureLoopDetected.withError(error), frequency: .legacyDailyAndCount, options: .withRetry) } } } @@ -744,7 +676,7 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { do { let vpnFileStoreDirectory = try Self.vpnFileStoreDirectory() let pixelKitStore = Self.setupPixelKit(vpnFileStoreDirectory: vpnFileStoreDirectory) - let legacyStores = Self.configureDailyPixelFileStore(vpnFileStoreDirectory: vpnFileStoreDirectory) + let legacyStores = Self.configureLegacyPixelFileStores(vpnFileStoreDirectory: vpnFileStoreDirectory) // The tunnel keeps its legacy daily and once-ever pixel state in its own app-group // files, not in the suites the browser migrates, so it runs its own migration here @@ -812,7 +744,9 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { return directory } - private static func configureDailyPixelFileStore(vpnFileStoreDirectory: URL?) -> (daily: KeyValueFileStore?, unique: KeyValueFileStore?) { + /// Reads the tunnel process's own legacy daily/once-ever pixel file stores, for `LegacyPixelStateMigration` + /// to copy into PixelKit. Nothing fires through these any more, so this only ever reads them. + private static func configureLegacyPixelFileStores(vpnFileStoreDirectory: URL?) -> (daily: KeyValueFileStore?, unique: KeyValueFileStore?) { guard let vpnFileStoreDirectory else { return (daily: nil, unique: nil) } let dailyPixelFileStore = try? KeyValueFileStore( @@ -820,18 +754,12 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { name: "daily-pixel", writeOptions: [.atomic, .noFileProtection] ) - if let dailyPixelFileStore { - DailyPixel.storage = dailyPixelFileStore - } let uniquePixelFileStore = try? KeyValueFileStore( location: vpnFileStoreDirectory, name: "unique-pixel", writeOptions: [.atomic, .noFileProtection] ) - if let uniquePixelFileStore { - UniquePixel.storage = uniquePixelFileStore - } return (daily: dailyPixelFileStore, unique: uniquePixelFileStore) } diff --git a/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPersistentPixel.swift b/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPersistentPixel.swift deleted file mode 100644 index b7615abb84f..00000000000 --- a/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPersistentPixel.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// MockPersistentPixel.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -@testable import Core - -final class MockPersistentPixel: PersistentPixelFiring { - - var expectedFireError: Error? - var expectedDailyPixelStorageError: Error? - var expectedCountPixelStorageError: Error? - - var lastPixelInfo: PixelInfo? - var lastDailyPixelInfo: PixelInfo? - - var lastParams: [String: String]? { lastPixelInfo?.params } - var lastPixelName: String? { lastPixelInfo?.pixelName } - var lastIncludedParams: [Pixel.QueryParameters]? { lastPixelInfo?.includedParams } - - func tearDown() { - lastPixelInfo = nil - lastDailyPixelInfo = nil - expectedFireError = nil - expectedDailyPixelStorageError = nil - expectedCountPixelStorageError = nil - } - - func fire(pixel: Core.Pixel.Event, - error: (any Error)?, - includedParameters: [Core.Pixel.QueryParameters], - withAdditionalParameters params: [String: String], - onComplete: @escaping ((any Error)?) -> Void) { - self.lastPixelInfo = .init(pixelName: pixel.name, error: error, params: params, includedParams: includedParameters) - onComplete(expectedFireError) - } - - func fireDailyAndCount(pixel: Core.Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String), - error: (any Error)?, - withAdditionalParameters params: [String: String], - includedParameters: [Core.Pixel.QueryParameters], - completion: @escaping ((dailyPixelStorageError: Error?, countPixelStorageError: Error?)) -> Void) { - self.lastDailyPixelInfo = .init(pixelName: pixel.name, error: error, params: params, includedParams: includedParameters) - completion((expectedDailyPixelStorageError, expectedCountPixelStorageError)) - } - - func sendQueuedPixels(completion: @escaping (Core.PersistentPixelStorageError?) -> Void) { - completion(nil) - } - -} diff --git a/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPixelFiring.swift b/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPixelFiring.swift deleted file mode 100644 index f0a6d3ee137..00000000000 --- a/iOS/SharedTestUtils/Mocks/Core/Statistics/MockPixelFiring.swift +++ /dev/null @@ -1,226 +0,0 @@ -// -// MockPixelFiring.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Core -import Networking -import Persistence - -struct PixelInfo { - let pixelName: String? - let error: Error? - let params: [String: String]? - let includedParams: [Pixel.QueryParameters]? - - init(pixelName: String?, - error: Error? = nil, - params: [String: String]?, - includedParams: [Pixel.QueryParameters]?) { - self.pixelName = pixelName - self.error = error - self.params = params - self.includedParams = includedParams - } -} - -final actor PixelFiringMock: PixelFiring, PixelFiringAsync, DailyPixelFiring { - - static var expectedFireError: Error? - static var expectedDailyPixelFireError: Error? - static var expectedCountPixelFireError: Error? - - static var allPixelsFired = [PixelInfo]() - - static var lastPixelInfo: PixelInfo? - static var lastDailyPixelInfo: PixelInfo? - - static var lastParams: [String: String]? { lastPixelInfo?.params } - static var lastPixelName: String? { lastPixelInfo?.pixelName } - static var lastIncludedParams: [Pixel.QueryParameters]? { lastPixelInfo?.includedParams } - - static func fire(pixel: Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters]) async throws { - let info = PixelInfo(pixelName: pixel.name, params: params, includedParams: includedParameters) - lastPixelInfo = info - allPixelsFired.append(info) - - if let expectedFireError { - throw expectedFireError - } - } - - static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping (Error?) -> Void) { - let info = PixelInfo(pixelName: pixel.name, params: params, includedParams: includedParameters) - lastPixelInfo = info - allPixelsFired.append(info) - - onComplete(expectedFireError) - } - - static func fire(_ pixel: Pixel.Event, - withAdditionalParameters params: [String: String]) { - let info = PixelInfo(pixelName: pixel.name, params: params, includedParams: nil) - lastPixelInfo = info - allPixelsFired.append(info) - } - - static func fire(pixel: Pixel.Event, - error: (any Error)?, - includedParameters: [Pixel.QueryParameters], - withAdditionalParameters params: [String: String], - onComplete: @escaping ((any Error)?) -> Void) { - - let info = PixelInfo(pixelName: pixel.name, error: error, params: params, includedParams: includedParameters) - lastPixelInfo = info - allPixelsFired.append(info) - - onComplete(expectedFireError) - } - - static func fire(pixelNamed pixelName: String, - withAdditionalParameters params: [String: String], - includedParameters: [Pixel.QueryParameters], - onComplete: @escaping ((any Error)?) -> Void) { - - let info = PixelInfo(pixelName: pixelName, params: params, includedParams: includedParameters) - lastPixelInfo = info - allPixelsFired.append(info) - - onComplete(expectedFireError) - } - - // DailyPixelFiring - - static func fireDaily(_ pixel: Pixel.Event) { - let info = PixelInfo(pixelName: pixel.name, params: nil, includedParams: nil) - lastDailyPixelInfo = info - allPixelsFired.append(info) - } - - static func fireDaily(_ pixel: Pixel.Event, withAdditionalParameters params: [String: String]) { - let info = PixelInfo(pixelName: pixel.name, params: params, includedParams: nil) - lastDailyPixelInfo = info - allPixelsFired.append(info) - } - - static func fireDailyAndCount(pixel: Pixel.Event, - pixelNameSuffixes: (dailySuffix: String, countSuffix: String), - error: (any Error)?, - withAdditionalParameters params: [String: String], - includedParameters: [Core.Pixel.QueryParameters], - pixelFiring: any PixelFiring.Type, - dailyPixelStore: any Persistence.ThrowingKeyValueStoring, - onDailyComplete: @escaping ((any Error)?) -> Void, - onCountComplete: @escaping ((any Error)?) -> Void) { - lastDailyPixelInfo = PixelInfo(pixelName: pixel.name, error: error, params: params, includedParams: includedParameters) - - onDailyComplete(expectedDailyPixelFireError) - onCountComplete(expectedCountPixelFireError) - } - - static func fireDailyAndCount(_ pixel: Core.Pixel.Event, error: (any Error)?, withAdditionalParameters params: [String: String]) { - lastDailyPixelInfo = PixelInfo(pixelName: pixel.name, error: error, params: params, includedParams: nil) - } - - // - - - static func tearDown() { - allPixelsFired = [] - lastPixelInfo = nil - lastDailyPixelInfo = nil - expectedFireError = nil - expectedDailyPixelFireError = nil - expectedCountPixelFireError = nil - } - - private init() {} -} - -class DelayedPixelFiringMock: PixelFiring { - - static var lastPixelInfo: PixelInfo? - static var lastParams: [String: String]? { lastPixelInfo?.params } - static var lastPixel: String? { lastPixelInfo?.pixelName } - static var lastIncludedParams: [Pixel.QueryParameters]? { lastPixelInfo?.includedParams } - static var completionHandlerUpdateClosure: ((Int) -> Void)? - - static var completionError: Error? - static var lastCompletionHandlers: [(Error?) -> Void] = [] { - didSet { - completionHandlerUpdateClosure?(lastCompletionHandlers.count) - } - } - - static func tearDown() { - lastPixelInfo = nil - completionError = nil - completionHandlerUpdateClosure = nil - lastCompletionHandlers = [] - } - - static func callCompletionHandler() { - for completionHandler in lastCompletionHandlers { - completionHandler(completionError) - } - } - - static func fire(_ pixel: Core.Pixel.Event, - withAdditionalParameters params: [String: String], - includedParameters: [Core.Pixel.QueryParameters], - onComplete: @escaping ((any Error)?) -> Void) { - self.fire(pixelNamed: pixel.name, withAdditionalParameters: params, includedParameters: includedParameters, onComplete: onComplete) - } - - static func fire(pixelNamed pixelName: String, - withAdditionalParameters params: [String: String], - includedParameters: [Core.Pixel.QueryParameters], - onComplete: @escaping ((any Error)?) -> Void) { - lastPixelInfo = PixelInfo(pixelName: pixelName, error: nil, params: params, includedParams: includedParameters) - lastCompletionHandlers.append(onComplete) - } - - static func fire(_ pixel: Core.Pixel.Event, withAdditionalParameters params: [String: String]) { - lastPixelInfo = PixelInfo(pixelName: pixel.name, error: nil, params: params, includedParams: nil) - } - - static func fire(pixelNamed pixelName: String, - forDeviceType deviceType: UIUserInterfaceIdiom?, - withAdditionalParameters params: [String: String], - allowedQueryReservedCharacters: CharacterSet?, - withHeaders headers: Networking.APIRequest.Headers, - includedParameters: [Core.Pixel.QueryParameters], - onComplete: @escaping ((any Error)?) -> Void) { - lastPixelInfo = PixelInfo(pixelName: pixelName, error: nil, params: params, includedParams: includedParameters) - lastCompletionHandlers.append(onComplete) - } - - static func fire(pixel: Pixel.Event, - error: Error?, - includedParameters: [Pixel.QueryParameters], - withAdditionalParameters params: [String: String], - onComplete: @escaping (Error?) -> Void) { - lastPixelInfo = PixelInfo(pixelName: pixel.name, error: nil, params: params, includedParams: includedParameters) - lastCompletionHandlers.append(onComplete) - } - -} diff --git a/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tutorials/Onboarding/OnboardingFirePixelMock.swift b/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tutorials/Onboarding/OnboardingFirePixelMock.swift deleted file mode 100644 index 9627c181e9f..00000000000 --- a/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tutorials/Onboarding/OnboardingFirePixelMock.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// OnboardingFirePixelMock.swift -// DuckDuckGo -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Core -import PrivacyConfig -@testable import DuckDuckGo - -final class OnboardingPixelFireMock: OnboardingPixelFiring { - static private(set) var didCallFire = false - static private(set) var capturedPixelEvent: Pixel.Event? - static private(set) var capturedParams: [String: String] = [:] - static private(set) var capturedIncludeParameters: [Pixel.QueryParameters] = [] - - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { - didCallFire = true - capturedPixelEvent = pixel - capturedParams = params - capturedIncludeParameters = includedParameters - } - - static func tearDown() { - didCallFire = false - capturedPixelEvent = nil - capturedParams = [:] - capturedIncludeParameters = [] - } -} - -final class OnboardingUniquePixelFireMock: OnboardingPixelFiring { - static private(set) var didCallFire = false - static private(set) var capturedPixelEvent: Pixel.Event? - static private(set) var capturedParams: [String: String] = [:] - static private(set) var capturedIncludeParameters: [Pixel.QueryParameters] = [] - - static private(set) var capturedPixelEventHistory: [Pixel.Event] = [] - - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String], includedParameters: [Pixel.QueryParameters]) { - didCallFire = true - capturedPixelEvent = pixel - capturedParams = params - capturedIncludeParameters = includedParameters - capturedPixelEventHistory.append(pixel) - } - - static func tearDown() { - didCallFire = false - capturedPixelEvent = nil - capturedParams = [:] - capturedIncludeParameters = [] - capturedPixelEventHistory = [] - } -} From e5ebe827ed802ca4fac61c55718863a685e1f87a Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Tue, 1 Sep 2026 14:26:13 +0100 Subject: [PATCH 2/3] lint --- iOS/Core/Pixel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/iOS/Core/Pixel.swift b/iOS/Core/Pixel.swift index d16b0c35f22..8407a15ae34 100644 --- a/iOS/Core/Pixel.swift +++ b/iOS/Core/Pixel.swift @@ -236,4 +236,3 @@ public struct PixelParameters { public static let suggestionsPageType = "pageType" public static let suggestionsAreSmart = "isSmart" } - From 5f0be479dbe2c1edba1d0b6427390a734c2ec6a1 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Wed, 2 Sep 2026 14:19:37 +0100 Subject: [PATCH 3/3] Fix stale comment after configureDailyPixelFileStore rename 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 --- .../NetworkProtectionPacketTunnelProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift b/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift index 1a4ba1e444e..8338912cba5 100644 --- a/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift +++ b/iOS/PacketTunnelProvider/NetworkProtection/NetworkProtectionPacketTunnelProvider.swift @@ -696,8 +696,8 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider { _ = Self.setupPixelKit(vpnFileStoreDirectory: nil) PixelKit.fire(Pixel.Event.networkProtectionPixelStorageSetupFailure.withError(error)) - // configureDailyPixelFileStore never ran, so DailyPixel/UniquePixel are still on their - // default UserDefaults suites - migrate those into the same fallback destination. + // configureLegacyPixelFileStores never ran, so migrate whatever a pre-migration install + // left in the legacy daily/unique UserDefaults suites into the same fallback destination. let destination: ThrowingKeyValueStoring = UserDefaults.networkProtectionGroupDefaults LegacyPixelStateMigration( destination: destination,