Skip to content

Commit

Permalink
Report Apple Ad attribution using pixel (#2510)
Browse files Browse the repository at this point in the history
  • Loading branch information
dus7 authored Mar 1, 2024
1 parent 3fbcab6 commit 62c54ab
Show file tree
Hide file tree
Showing 10 changed files with 742 additions and 1 deletion.
9 changes: 9 additions & 0 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ 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 {
Expand Down
5 changes: 5 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,8 @@ extension Pixel {

case appRatingPromptFetchError

case appleAdAttribution

case userBehaviorReloadTwice
case userBehaviorReloadAndRestart
case userBehaviorReloadAndFireButton
Expand Down Expand Up @@ -1032,6 +1034,9 @@ 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"
Expand Down
3 changes: 2 additions & 1 deletion Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ public struct UserDefaultsWrapper<T> {

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
Expand Down
36 changes: 36 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,12 @@
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 */; };
Expand Down Expand Up @@ -1399,7 +1404,12 @@
6AC6DAB228804F97002723C0 /* BarsAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarsAnimator.swift; sourceTree = "<group>"; };
6AC98418288055C1005FA9CA /* BarsAnimatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarsAnimatorTests.swift; sourceTree = "<group>"; };
6FB030C7234331B400A10DB9 /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = Configuration/Configuration.xcconfig; sourceTree = "<group>"; };
6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = "<group>"; };
6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = "<group>"; };
6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = "<group>"; };
6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = "<group>"; };
6FDA1FB22B59584400AC962A /* AddressDisplayHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDisplayHelper.swift; sourceTree = "<group>"; };
6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = "<group>"; };
83004E7F2193BB8200DA013C /* WKNavigationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationExtension.swift; sourceTree = "<group>"; };
83004E832193E14C00DA013C /* UIAlertControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UIAlertControllerExtension.swift; path = ../Core/UIAlertControllerExtension.swift; sourceTree = "<group>"; };
83004E852193E5ED00DA013C /* TabViewControllerBrowsingMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewControllerBrowsingMenuExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3545,6 +3555,25 @@
name = VPN;
sourceTree = "<group>";
};
6FD1BAE02B87A0E8000C475C /* AdAttribution */ = {
isa = PBXGroup;
children = (
6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */,
6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */,
6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */,
);
name = AdAttribution;
sourceTree = "<group>";
};
6FF9157F2B88E04F0042AC87 /* AdAttribution */ = {
isa = PBXGroup;
children = (
6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */,
6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */,
);
name = AdAttribution;
sourceTree = "<group>";
};
830FA79B1F8E81FB00FCE105 /* ContentBlocker */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3730,6 +3759,7 @@
CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */,
EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */,
CB258D1129A4F1BB00DEBA24 /* Configuration */,
6FD1BAE02B87A0E8000C475C /* AdAttribution */,
1E908BED29827C480008C8F3 /* Autoconsent */,
3157B43627F4C8380042D3D7 /* Favicons */,
AA4D6A8023DE4973007E8790 /* AppIcon */,
Expand Down Expand Up @@ -4936,6 +4966,7 @@
F12D98401F266B30003C2EE3 /* DuckDuckGo */ = {
isa = PBXGroup;
children = (
6FF9157F2B88E04F0042AC87 /* AdAttribution */,
CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */,
F17669A21E411D63003D3222 /* Application */,
026F08B629B7DC130079B9DF /* AppTrackingProtection */,
Expand Down Expand Up @@ -6485,6 +6516,7 @@
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 */,
Expand Down Expand Up @@ -6661,6 +6693,7 @@
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 */,
Expand Down Expand Up @@ -6881,6 +6914,7 @@
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 */,
Expand Down Expand Up @@ -6959,6 +6993,7 @@
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 */,
Expand Down Expand Up @@ -7035,6 +7070,7 @@
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 */,
Expand Down
159 changes: 159 additions & 0 deletions DuckDuckGo/AdAttribution/AdAttributionFetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//
// 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..<Constant.maxRetries {
do {
try Task.checkCancellation()

let token = try (lastToken ?? tokenGetter())
lastToken = token
return try await fetchAttributionData(using: token)
} catch let error as AdAttributionFetcherError {
os_log("AdAttributionFetcher failed to fetch attribution data: %@. Retrying.", log: .adAttributionLog, error.localizedDescription)

if error == .invalidToken {
lastToken = nil
}

if error.allowsRetry {
try? await Task.sleep(interval: retryInterval)
continue
} else {
break
}
} catch {
os_log("AdAttributionFetcher failed to fetch attribution data: %@", log: .adAttributionLog, error.localizedDescription)

// Do not retry
break
}
}

return nil
}

private func fetchAttributionData(using token: String) async throws -> 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
}
}
}
Loading

0 comments on commit 62c54ab

Please sign in to comment.