From cb0877d0fdf3a06a80893a23277f81acb0481002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Fri, 8 Mar 2024 16:30:26 +0100 Subject: [PATCH] Revert "Report Apple Ad attribution using pixel (#2510)" (#2562) --- Core/Pixel.swift | 9 - Core/PixelEvent.swift | 5 - Core/UserDefaultsPropertyWrapper.swift | 3 +- DuckDuckGo.xcodeproj/project.pbxproj | 36 --- .../AdAttribution/AdAttributionFetcher.swift | 159 -------------- .../AdAttributionPixelReporter.swift | 96 -------- .../AdAttributionReporterStorage.swift | 38 ---- DuckDuckGo/AppDelegate.swift | 8 - .../AdAttributionFetcherTests.swift | 183 ---------------- .../AdAttributionPixelReporterTests.swift | 206 ------------------ 10 files changed, 1 insertion(+), 742 deletions(-) delete mode 100644 DuckDuckGo/AdAttribution/AdAttributionFetcher.swift delete mode 100644 DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift delete mode 100644 DuckDuckGo/AdAttribution/AdAttributionReporterStorage.swift delete mode 100644 DuckDuckGoTests/AdAttributionFetcherTests.swift delete mode 100644 DuckDuckGoTests/AdAttributionPixelReporterTests.swift diff --git a/Core/Pixel.swift b/Core/Pixel.swift index edf6d6611b..33b3ca2214 100644 --- a/Core/Pixel.swift +++ b/Core/Pixel.swift @@ -127,15 +127,6 @@ public struct PixelParameters { public static let returnUserErrorCode = "error_code" public static let returnUserOldATB = "old_atb" public static let returnUserNewATB = "new_atb" - - // Ad Attribution - public static let adAttributionOrgID = "org_id" - public static let adAttributionCampaignID = "campaign_id" - public static let adAttributionConversionType = "conversion_type" - public static let adAttributionAdGroupID = "ad_group_id" - public static let adAttributionCountryOrRegion = "country_or_region" - public static let adAttributionKeywordID = "keyword_id" - public static let adAttributionAdID = "ad_id" } public struct PixelValues { diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 8925cb5716..a4e38df468 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -541,8 +541,6 @@ extension Pixel { case appRatingPromptFetchError - case appleAdAttribution - case userBehaviorReloadTwice case userBehaviorReloadAndRestart case userBehaviorReloadAndFireButton @@ -1059,9 +1057,6 @@ extension Pixel.Event { case .debugReturnUserUpdateATB: return "m_debug_return_user_update_atb" case .appRatingPromptFetchError: return "m_d_app_rating_prompt_fetch_error" - - // MARK: - Apple Ad Attribution - case .appleAdAttribution: return "m_apple-ad-attribution" // MARK: - User behavior case .userBehaviorReloadTwice: return "m_reload-twice" diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index f0515137b2..9ab2131af7 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -124,10 +124,9 @@ public struct UserDefaultsWrapper { case subscriptionIsActive = "com.duckduckgo.ios.subscruption.isActive" - case appleAdAttributionReportCompleted = "com.duckduckgo.ios.appleAdAttributionReport.completed" - case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp" case didBurnTimestamp = "com.duckduckgo.ios.userBehavior.didBurnTimestamp" + } private let key: Key diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 118bd1271b..ed21691428 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -309,12 +309,7 @@ 4BFB911B29B7D9530014D4B7 /* AppTrackingProtectionStoringModelPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFB911A29B7D9530014D4B7 /* AppTrackingProtectionStoringModelPerformanceTests.swift */; }; 6AC6DAB328804F97002723C0 /* BarsAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC6DAB228804F97002723C0 /* BarsAnimator.swift */; }; 6AC98419288055C1005FA9CA /* BarsAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98418288055C1005FA9CA /* BarsAnimatorTests.swift */; }; - 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */; }; - 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */; }; - 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */; }; - 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */; }; 6FDA1FB32B59584400AC962A /* AddressDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */; }; - 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; 83004E802193BB8200DA013C /* WKNavigationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */; }; 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */; }; 83004E882193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83004E872193E8C700DA013C /* TabViewControllerLongPressMenuExtension.swift */; }; @@ -1404,12 +1399,7 @@ 6AC6DAB228804F97002723C0 /* BarsAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarsAnimator.swift; sourceTree = ""; }; 6AC98418288055C1005FA9CA /* BarsAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarsAnimatorTests.swift; sourceTree = ""; }; 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = ""; }; - 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; - 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; - 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; - 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = ""; }; - 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; 83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = ""; }; 83004E832193E14C00DA013C /* UIAlertControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIAlertControllerExtension.swift; path = ../Core/UIAlertControllerExtension.swift; sourceTree = ""; }; 83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerBrowsingMenuExtension.swift; sourceTree = ""; }; @@ -3555,25 +3545,6 @@ name = VPN; sourceTree = ""; }; - 6FD1BAE02B87A0E8000C475C /* AdAttribution */ = { - isa = PBXGroup; - children = ( - 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */, - 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */, - 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */, - ); - name = AdAttribution; - sourceTree = ""; - }; - 6FF9157F2B88E04F0042AC87 /* AdAttribution */ = { - isa = PBXGroup; - children = ( - 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */, - 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */, - ); - name = AdAttribution; - sourceTree = ""; - }; 830FA79B1F8E81FB00FCE105 /* ContentBlocker */ = { isa = PBXGroup; children = ( @@ -3759,7 +3730,6 @@ CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */, EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */, CB258D1129A4F1BB00DEBA24 /* Configuration */, - 6FD1BAE02B87A0E8000C475C /* AdAttribution */, 1E908BED29827C480008C8F3 /* Autoconsent */, 3157B43627F4C8380042D3D7 /* Favicons */, AA4D6A8023DE4973007E8790 /* AppIcon */, @@ -4966,7 +4936,6 @@ F12D98401F266B30003C2EE3 /* DuckDuckGo */ = { isa = PBXGroup; children = ( - 6FF9157F2B88E04F0042AC87 /* AdAttribution */, CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */, F17669A21E411D63003D3222 /* Application */, 026F08B629B7DC130079B9DF /* AppTrackingProtection */, @@ -6516,7 +6485,6 @@ 319A371028299A850079FBCE /* PasswordHider.swift in Sources */, 982C87C42255559A00919035 /* UITableViewCellExtension.swift in Sources */, B623C1C42862CD670043013E /* WKDownloadSession.swift in Sources */, - 6FD1BAE42B87A107000C475C /* AdAttributionPixelReporter.swift in Sources */, EEFD562F2A65B6CA00DAEC48 /* NetworkProtectionInviteViewModel.swift in Sources */, 1E8AD1D927C4FEC100ABA377 /* DownloadsListSectioningHelper.swift in Sources */, 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */, @@ -6693,7 +6661,6 @@ 31CB4251273AF50700FA0F3F /* SpeechRecognizerProtocol.swift in Sources */, 319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */, 85EE7F59224673C5000FE757 /* WebContainerNavigationController.swift in Sources */, - 6FD1BAE52B87A107000C475C /* AdAttributionReporterStorage.swift in Sources */, D68A21462B7EC16200BB372E /* SubscriptionExternalLinkViewModel.swift in Sources */, F4C9FBF528340DDA002281CC /* AutofillInterfaceEmailTruncator.swift in Sources */, 1E016AB42949FEB500F21625 /* OmniBarNotificationViewModel.swift in Sources */, @@ -6914,7 +6881,6 @@ 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */, D6D95CE12B6D52DA00960317 /* RootPresentationMode.swift in Sources */, 83004E862193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift in Sources */, - 6FD1BAE62B87A107000C475C /* AdAttributionFetcher.swift in Sources */, EE01EB402AFBD0000096AAC9 /* NetworkProtectionVPNSettingsViewModel.swift in Sources */, EE72CA852A862D000043B5B3 /* NetworkProtectionDebugViewController.swift in Sources */, C18ED43A2AB6F77600BF3805 /* AutofillSettingsEnableFooterView.swift in Sources */, @@ -6993,7 +6959,6 @@ CBDD5DDF29A6736A00832877 /* APIHeadersTests.swift in Sources */, 986B45D0299E30A50089D2D7 /* BookmarkEntityTests.swift in Sources */, B6AD9E3828D4512E0019CDE9 /* EmbeddedTrackerDataTests.swift in Sources */, - 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */, 1E722729292EB24D003B5F53 /* AppSettingsMock.swift in Sources */, 8536A1C8209AF2410050739E /* MockVariantManager.swift in Sources */, C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, @@ -7070,7 +7035,6 @@ 9847C00527A41A0A00DB07AA /* WebViewTestHelper.swift in Sources */, 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */, 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */, - 6FD3AEE32B8F4EEB0060FCCC /* AdAttributionPixelReporterTests.swift in Sources */, 987130C6294AAB9F00AB05E0 /* BookmarkListViewModelTests.swift in Sources */, F1134ED21F40EF3A00B73467 /* JsonTestDataLoader.swift in Sources */, 4B83397129AC18C9003F7EA9 /* AppTrackingProtectionStoringModelTests.swift in Sources */, diff --git a/DuckDuckGo/AdAttribution/AdAttributionFetcher.swift b/DuckDuckGo/AdAttribution/AdAttributionFetcher.swift deleted file mode 100644 index a9d3d31a80..0000000000 --- a/DuckDuckGo/AdAttribution/AdAttributionFetcher.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// AdAttributionFetcher.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 AdServices -import Common - -protocol AdAttributionFetcher { - func fetch() async -> AdServicesAttributionResponse? -} - -/// Fetches ad attribution data for from Apple. -/// -/// DuckDuckGo uses the AdServices framework to fetch and monitor anonymous install attribution data from Apple. No personally identifiable data is involved. -/// DuckDuckGo does not use the App Tracking Transparency framework at any point, and only uses the “standard” attribution payload. -/// See https://developer.apple.com/documentation/adservices/aaattribution/attributiontoken()#Attribution-payload-descriptions for details. -struct DefaultAdAttributionFetcher: AdAttributionFetcher { - - typealias TokenGetter = () throws -> String - - private let tokenGetter: TokenGetter - private let urlSession: URLSession - private let retryInterval: TimeInterval - - init(tokenGetter: @escaping TokenGetter = Self.fetchAttributionToken, - urlSession: URLSession = .shared, - retryInterval: TimeInterval = .seconds(5)) { - self.tokenGetter = tokenGetter - self.urlSession = urlSession - self.retryInterval = retryInterval - } - - func fetch() async -> AdServicesAttributionResponse? { - guard #available(iOS 14.3, *) else { - return nil - } - - var lastToken: String? - - for _ in 0.. AdServicesAttributionResponse { - let request = createAttributionDataRequest(with: token) - let (data, response) = try await urlSession.data(for: request) - - guard let response = response as? HTTPURLResponse else { - throw AdAttributionFetcherError.invalidResponse - } - - switch response.statusCode { - case 200: - let decoder = JSONDecoder() - let decoded = try decoder.decode(AdServicesAttributionResponse.self, from: data) - - return decoded - case 400: - throw AdAttributionFetcherError.invalidToken - case 404: - throw AdAttributionFetcherError.invalidResponse - default: - throw AdAttributionFetcherError.unknown - } - } - - private func createAttributionDataRequest(with token: String) -> URLRequest { - var request = URLRequest(url: Constant.attributionServiceURL) - request.httpMethod = "POST" - request.setValue("text/plain", forHTTPHeaderField: "Content-Type") - request.httpBody = token.data(using: .utf8) - - return request - } - - private struct Constant { - static let attributionServiceURL = URL(string: "https://api-adservices.apple.com/api/v1/")! - static let maxRetries = 3 - } -} - -extension AdAttributionFetcher { - static func fetchAttributionToken() throws -> String { - if #available(iOS 14.3, *) { - return try AAAttribution.attributionToken() - } else { - throw AdAttributionFetcherError.attributionUnsupported - } - } -} - -struct AdServicesAttributionResponse: Decodable { - let attribution: Bool - let orgId: Int? - let campaignId: Int? - let conversionType: String? - let adGroupId: Int? - let countryOrRegion: String? - let keywordId: Int? - let adId: Int? -} - -enum AdAttributionFetcherError: Error { - case attributionUnsupported - case invalidResponse - case invalidToken - case unknown - - var allowsRetry: Bool { - switch self { - case .invalidToken, .invalidResponse: - return true - case .unknown, .attributionUnsupported: - return false - } - } -} diff --git a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift b/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift deleted file mode 100644 index 4ea0faa969..0000000000 --- a/DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// AdAttributionPixelReporter.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 - -protocol PixelFiring { - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String]) async throws -} - -final class AdAttributionPixelReporter { - - static var shared = AdAttributionPixelReporter() - - private var fetcherStorage: AdAttributionReporterStorage - private let attributionFetcher: AdAttributionFetcher - private let pixelFiring: PixelFiring.Type - - init(fetcherStorage: AdAttributionReporterStorage = UserDefaultsAdAttributionReporterStorage(), - attributionFetcher: AdAttributionFetcher = DefaultAdAttributionFetcher(), - pixelFiring: PixelFiring.Type = Pixel.self) { - self.fetcherStorage = fetcherStorage - self.attributionFetcher = attributionFetcher - self.pixelFiring = pixelFiring - } - - @discardableResult - func reportAttributionIfNeeded() async -> Bool { - guard await fetcherStorage.wasAttributionReportSuccessful == false else { - return false - } - - if let attributionData = await self.attributionFetcher.fetch() { - if attributionData.attribution { - let parameters = self.pixelParametersForAttribution(attributionData) - do { - try await pixelFiring.fire(pixel: .appleAdAttribution, withAdditionalParameters: parameters) - } catch { - return false - } - } - - await fetcherStorage.markAttributionReportSuccessful() - - return true - } - - return false - } - - private func pixelParametersForAttribution(_ attribution: AdServicesAttributionResponse) -> [String: String] { - var params: [String: String] = [:] - - params[PixelParameters.adAttributionAdGroupID] = attribution.adGroupId.map(String.init) - params[PixelParameters.adAttributionOrgID] = attribution.orgId.map(String.init) - params[PixelParameters.adAttributionCampaignID] = attribution.campaignId.map(String.init) - params[PixelParameters.adAttributionConversionType] = attribution.conversionType - params[PixelParameters.adAttributionAdGroupID] = attribution.adGroupId.map(String.init) - params[PixelParameters.adAttributionCountryOrRegion] = attribution.countryOrRegion - params[PixelParameters.adAttributionKeywordID] = attribution.keywordId.map(String.init) - params[PixelParameters.adAttributionAdID] = attribution.adId.map(String.init) - - return params - } -} - -extension Pixel: PixelFiring { - static func fire(pixel: Event, withAdditionalParameters params: [String: String]) async throws { - - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - Pixel.fire(pixel: pixel, withAdditionalParameters: params) { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } -} diff --git a/DuckDuckGo/AdAttribution/AdAttributionReporterStorage.swift b/DuckDuckGo/AdAttribution/AdAttributionReporterStorage.swift deleted file mode 100644 index c0085e05f9..0000000000 --- a/DuckDuckGo/AdAttribution/AdAttributionReporterStorage.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// AdAttributionReporterStorage.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 Core -import Foundation - -protocol AdAttributionReporterStorage { - var wasAttributionReportSuccessful: Bool { get async } - - func markAttributionReportSuccessful() async -} - -final class UserDefaultsAdAttributionReporterStorage: AdAttributionReporterStorage { - @MainActor - @UserDefaultsWrapper(key: .appleAdAttributionReportCompleted, defaultValue: false) - var wasAttributionReportSuccessful: Bool - - @MainActor - func markAttributionReportSuccessful() async { - wasAttributionReportSuccessful = true - } -} diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 3bd12868ed..1f712dd2c6 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -335,8 +335,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { clearDebugWaitlistState() - reportAdAttribution() - AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp) return true @@ -389,12 +387,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } #endif - private func reportAdAttribution() { - Task.detached(priority: .background) { - await AdAttributionPixelReporter.shared.reportAttributionIfNeeded() - } - } - func applicationDidBecomeActive(_ application: UIApplication) { guard !testing else { return } diff --git a/DuckDuckGoTests/AdAttributionFetcherTests.swift b/DuckDuckGoTests/AdAttributionFetcherTests.swift deleted file mode 100644 index 28b9c94541..0000000000 --- a/DuckDuckGoTests/AdAttributionFetcherTests.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// AdAttributionFetcherTests.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 XCTest - -@testable import DuckDuckGo -@testable import TestUtils - -final class AdAttributionFetcherTests: XCTestCase { - - private let mockSession: URLSession = { - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [MockURLProtocol.self] - let session = URLSession(configuration: configuration) - - return session - }() - - override func setUpWithError() throws { - MockURLProtocol.requestHandler = MockURLProtocol.defaultHandler - } - - override func tearDownWithError() throws { - MockURLProtocol.requestHandler = nil - } - - func testMakesRequestWithToken() async throws { - let testToken = "foo" - let sut = DefaultAdAttributionFetcher(tokenGetter: { testToken }, urlSession: mockSession, retryInterval: .leastNonzeroMagnitude) - - _ = await sut.fetch() - - let requestStream = try XCTUnwrap(MockURLProtocol.lastRequest?.httpBodyStream) - let requestBody = try Data(reading: requestStream) - - XCTAssertEqual(String(data: requestBody, encoding: .utf8), testToken) - } - - func testRetriesRequest() async throws { - let testToken = "foo" - let sut = DefaultAdAttributionFetcher(tokenGetter: { testToken }, urlSession: mockSession, retryInterval: .leastNonzeroMagnitude) - let retryExpectation = XCTestExpectation() - retryExpectation.expectedFulfillmentCount = 3 - retryExpectation.assertForOverFulfill = true - - MockURLProtocol.requestHandler = { request in - retryExpectation.fulfill() - let handler = MockURLProtocol.handler(with: 404) - return try handler(request) - } - - _ = await sut.fetch() - - let requestStream = try XCTUnwrap(MockURLProtocol.lastRequest?.httpBodyStream) - let requestBody = try Data(reading: requestStream) - - XCTAssertEqual(String(data: requestBody, encoding: .utf8), testToken) - - await fulfillment(of: [retryExpectation]) - } - - func testRefreshesTokenOnRetry() async throws { - let retryExpectation = XCTestExpectation() - retryExpectation.expectedFulfillmentCount = 3 - retryExpectation.assertForOverFulfill = true - - let refreshExpectation = XCTestExpectation() - - let testToken = "foo" - let sut = DefaultAdAttributionFetcher(tokenGetter: { - refreshExpectation.fulfill() - return testToken - }, urlSession: mockSession, retryInterval: .leastNonzeroMagnitude) - - MockURLProtocol.requestHandler = { request in - retryExpectation.fulfill() - let handler = MockURLProtocol.handler(with: 400) - return try handler(request) - } - - _ = await sut.fetch() - - let requestStream = try XCTUnwrap(MockURLProtocol.lastRequest?.httpBodyStream) - let requestBody = try Data(reading: requestStream) - - XCTAssertEqual(String(data: requestBody, encoding: .utf8), testToken) - - await fulfillment(of: [retryExpectation]) - } - - func testDoesNotRetry_WhenUnrecoverable() async throws { - let testToken = "foo" - let sut = DefaultAdAttributionFetcher(tokenGetter: { testToken }, urlSession: mockSession, retryInterval: .leastNonzeroMagnitude) - let noRetryExpectation = XCTestExpectation() - noRetryExpectation.expectedFulfillmentCount = 1 - noRetryExpectation.assertForOverFulfill = true - - MockURLProtocol.requestHandler = { request in - noRetryExpectation.fulfill() - let handler = MockURLProtocol.handler(with: 500) - return try handler(request) - } - - _ = await sut.fetch() - - let requestStream = try XCTUnwrap(MockURLProtocol.lastRequest?.httpBodyStream) - let requestBody = try Data(reading: requestStream) - - XCTAssertEqual(String(data: requestBody, encoding: .utf8), testToken) - - await fulfillment(of: [noRetryExpectation]) - } - - func testRespectsRetryInterval() async throws { - let testToken = "foo" - let sut = DefaultAdAttributionFetcher(tokenGetter: { testToken }, urlSession: mockSession, retryInterval: .milliseconds(30)) - - MockURLProtocol.requestHandler = { request in - let handler = MockURLProtocol.handler(with: 404) - return try handler(request) - } - - let startTime = Date() - _ = await sut.fetch() - - XCTAssertGreaterThanOrEqual(Date().timeIntervalSince(startTime), .milliseconds(90)) - } -} - -private extension MockURLProtocol { - typealias RequestHandler = (URLRequest) throws -> (HTTPURLResponse, Data?) - - static func handler(with statusCode: Int, data: Data? = nil) -> RequestHandler { - return { request in - (HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!, data) - } - } - - static let defaultHandler = handler(with: 300) -} - -private extension Data { - init(reading input: InputStream, size: Int = 1024) throws { - self.init() - input.open() - defer { - input.close() - } - - let bufferSize = size - let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) - defer { - buffer.deallocate() - } - while input.hasBytesAvailable { - let read = input.read(buffer, maxLength: bufferSize) - if read < 0 { - // Stream error occured - throw input.streamError! - } else if read == 0 { - // EOF - break - } - self.append(buffer, count: read) - } - } -} diff --git a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift b/DuckDuckGoTests/AdAttributionPixelReporterTests.swift deleted file mode 100644 index 97eef8546e..0000000000 --- a/DuckDuckGoTests/AdAttributionPixelReporterTests.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// AdAttributionPixelReporterTests.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 Core -import XCTest - -@testable import DuckDuckGo - -final class AdAttributionPixelReporterTests: XCTestCase { - - private var attributionFetcher: AdAttributionFetcherMock! - private var fetcherStorage: AdAttributionReporterStorageMock! - - override func setUpWithError() throws { - attributionFetcher = AdAttributionFetcherMock() - fetcherStorage = AdAttributionReporterStorageMock() - } - - override func tearDownWithError() throws { - attributionFetcher = nil - fetcherStorage = nil - PixelFiringMock.tearDown() - } - - func testReportsAttribution() async { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: true) - - let result = await sut.reportAttributionIfNeeded() - - XCTAssertEqual(PixelFiringMock.lastPixel, .appleAdAttribution) - XCTAssertTrue(result) - } - - func testReportsOnce() async { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: true) - - await fetcherStorage.markAttributionReportSuccessful() - let result = await sut.reportAttributionIfNeeded() - - XCTAssertNil(PixelFiringMock.lastPixel) - XCTAssertFalse(result) - } - - func testPixelname() async { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: true) - - let result = await sut.reportAttributionIfNeeded() - - XCTAssertEqual(PixelFiringMock.lastPixel?.name, "m_apple-ad-attribution") - XCTAssertTrue(result) - } - - func testPixelAttributesNaming() async throws { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: true) - - await sut.reportAttributionIfNeeded() - - let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) - - XCTAssertEqual(pixelAttributes["org_id"], "1") - XCTAssertEqual(pixelAttributes["campaign_id"], "2") - XCTAssertEqual(pixelAttributes["conversion_type"], "conversionType") - XCTAssertEqual(pixelAttributes["ad_group_id"], "3") - XCTAssertEqual(pixelAttributes["country_or_region"], "countryOrRegion") - XCTAssertEqual(pixelAttributes["keyword_id"], "4") - XCTAssertEqual(pixelAttributes["ad_id"], "5") - } - - func testPixelAttributes_WhenPartialAttributionData() async throws { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse( - attribution: true, - orgId: 1, - campaignId: 2, - conversionType: "conversionType", - adGroupId: nil, - countryOrRegion: nil, - keywordId: nil, - adId: nil - ) - - await sut.reportAttributionIfNeeded() - - let pixelAttributes = try XCTUnwrap(PixelFiringMock.lastParams) - - XCTAssertEqual(pixelAttributes["org_id"], "1") - XCTAssertEqual(pixelAttributes["campaign_id"], "2") - XCTAssertEqual(pixelAttributes["conversion_type"], "conversionType") - XCTAssertNil(pixelAttributes["ad_group_id"]) - XCTAssertNil(pixelAttributes["country_or_region"]) - XCTAssertNil(pixelAttributes["keyword_id"]) - XCTAssertNil(pixelAttributes["ad_id"]) - } - - func testPixelNotFiredAndMarksReport_WhenAttributionFalse() async { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: false) - - let result = await sut.reportAttributionIfNeeded() - - XCTAssertNil(PixelFiringMock.lastPixel) - XCTAssertTrue(fetcherStorage.wasAttributionReportSuccessful) - XCTAssertTrue(result) - } - - func testPixelNotFiredAndReportNotMarked_WhenAttributionUnavailable() async { - let sut = createSUT() - attributionFetcher.fetchResponse = nil - - let result = await sut.reportAttributionIfNeeded() - - XCTAssertNil(PixelFiringMock.lastPixel) - XCTAssertFalse(fetcherStorage.wasAttributionReportSuccessful) - XCTAssertFalse(result) - } - - func testDoesNotMarkSuccessful_WhenPixelFiringFailed() async { - let sut = createSUT() - attributionFetcher.fetchResponse = AdServicesAttributionResponse(attribution: true) - PixelFiringMock.expectedFireError = NSError(domain: "PixelFailure", code: 1) - - let result = await sut.reportAttributionIfNeeded() - - XCTAssertFalse(fetcherStorage.wasAttributionReportSuccessful) - XCTAssertFalse(result) - } - - private func createSUT() -> AdAttributionPixelReporter { - AdAttributionPixelReporter(fetcherStorage: fetcherStorage, - attributionFetcher: attributionFetcher, - pixelFiring: PixelFiringMock.self) - } -} - -class AdAttributionReporterStorageMock: AdAttributionReporterStorage { - func markAttributionReportSuccessful() async { - wasAttributionReportSuccessful = true - } - - private(set) var wasAttributionReportSuccessful: Bool = false -} - -class AdAttributionFetcherMock: AdAttributionFetcher { - var fetchResponse: AdServicesAttributionResponse? - func fetch() async -> AdServicesAttributionResponse? { - fetchResponse - } -} - -final actor PixelFiringMock: PixelFiring { - static var expectedFireError: Error? - - static var lastParams: [String: String]? - static var lastPixel: Pixel.Event? - static func fire(pixel: Pixel.Event, withAdditionalParameters params: [String: String]) async throws { - lastParams = params - lastPixel = pixel - - if let expectedFireError { - throw expectedFireError - } - } - - static func tearDown() { - lastParams = nil - lastPixel = nil - expectedFireError = nil - } - - private init() {} -} - -extension AdServicesAttributionResponse { - init(attribution: Bool) { - self.init( - attribution: attribution, - orgId: 1, - campaignId: 2, - conversionType: "conversionType", - adGroupId: 3, - countryOrRegion: "countryOrRegion", - keywordId: 4, - adId: 5 - ) - } -}