Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reapply "Report Apple Ad attribution using pixel" #2702

Merged
merged 4 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ public struct PixelParameters {

// Pixel Experiment
public static let cohort = "cohort"

// 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
7 changes: 7 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -652,10 +652,14 @@ extension Pixel {
case privacyProAddEmailSuccess
case privacyProWelcomeFAQClick

// MARK: Apple Ad Attribution
case appleAdAttribution

// MARK: Secure Vault
case secureVaultL1KeyMigration
case secureVaultL2KeyMigration
case secureVaultL2KeyPasswordMigration

}

}
Expand Down Expand Up @@ -1176,6 +1180,9 @@ extension Pixel.Event {
case .toggleReportDismiss: return "m_toggle-report-dismiss"

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: 3 additions & 0 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,15 @@ public struct UserDefaultsWrapper<T> {

case privacyProEnvironment = "com.duckduckgo.ios.privacyPro.environment"

case appleAdAttributionReportCompleted = "com.duckduckgo.ios.appleAdAttributionReport.completed"

case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp"
case didBurnTimestamp = "com.duckduckgo.ios.userBehavior.didBurnTimestamp"

case pixelExperimentInstalled = "com.duckduckgo.ios.pixel.experiment.installed"
case pixelExperimentCohort = "com.duckduckgo.ios.pixel.experiment.cohort"
case pixelExperimentEnrollmentDate = "com.duckduckgo.ios.pixel.experiment.enrollment.date"

}

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 @@ -240,7 +240,12 @@
6AC98419288055C1005FA9CA /* BarsAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC98418288055C1005FA9CA /* BarsAnimatorTests.swift */; };
6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; };
6F8496412BC3D8EE00ADA54E /* OnboardingButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.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 @@ -1289,7 +1294,12 @@
6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = "<group>"; };
6F8496402BC3D8EE00ADA54E /* OnboardingButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingButtonsView.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 @@ -3257,6 +3267,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 @@ -3443,6 +3472,7 @@
84E341941E2F7EFB00BDBA6F /* DuckDuckGo */ = {
isa = PBXGroup;
children = (
6FD1BAE02B87A0E8000C475C /* AdAttribution */,
AA4D6A8023DE4973007E8790 /* AppIcon */,
F1C5ECF31E37812900C599A4 /* Application */,
9817C9C121EF58BA00884F65 /* AutoClear */,
Expand Down Expand Up @@ -4658,6 +4688,7 @@
F12D98401F266B30003C2EE3 /* DuckDuckGo */ = {
isa = PBXGroup;
children = (
6FF9157F2B88E04F0042AC87 /* AdAttribution */,
CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */,
F17669A21E411D63003D3222 /* Application */,
981FED7222045FFA008488D7 /* AutoClear */,
Expand Down Expand Up @@ -6188,6 +6219,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 */,
D60170BD2BA34CE8001911B5 /* Subscription.swift in Sources */,
Expand Down Expand Up @@ -6369,6 +6401,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 @@ -6588,6 +6621,7 @@
31EF52E1281B3BDC0034796E /* AutofillLoginListItemViewModel.swift in Sources */,
1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.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 @@ -6662,6 +6696,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 @@ -6740,6 +6775,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 */,
850250B520D80419002199C7 /* AtbAndVariantCleanupTests.swift in Sources */,
Expand Down
3 changes: 1 addition & 2 deletions DuckDuckGo/AdAttribution/AdAttributionFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import AdServices
import Common
import Macros

protocol AdAttributionFetcher {
func fetch() async -> AdServicesAttributionResponse?
Expand Down Expand Up @@ -117,7 +116,7 @@ struct DefaultAdAttributionFetcher: AdAttributionFetcher {
}

private struct Constant {
static let attributionServiceURL = #URL("https://api-adservices.apple.com/api/v1/")
static let attributionServiceURL = URL(string: "https://api-adservices.apple.com/api/v1/")!
static let maxRetries = 3
}
}
Expand Down
98 changes: 98 additions & 0 deletions DuckDuckGo/AdAttribution/AdAttributionPixelReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// 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 let isAdAttributionReportingEnabled = false

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<Void, Error>) in
Pixel.fire(pixel: pixel, withAdditionalParameters: params) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
38 changes: 38 additions & 0 deletions DuckDuckGo/AdAttribution/AdAttributionReporterStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// 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
}
}
10 changes: 10 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

AppDependencyProvider.shared.toggleProtectionsCounter.sendEventsIfNeeded()

AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp)

return true
Expand Down Expand Up @@ -410,6 +411,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}

private func reportAdAttribution() {
guard AdAttributionPixelReporter.isAdAttributionReportingEnabled else { return }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've add flag here so it's easy to flip the switch once needed.


Task.detached(priority: .background) {
await AdAttributionPixelReporter.shared.reportAttributionIfNeeded()
}
}

func applicationDidBecomeActive(_ application: UIApplication) {
guard !testing else { return }

Expand All @@ -428,6 +437,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
StatisticsLoader.shared.refreshAppRetentionAtb()
self.fireAppLaunchPixel()
self.firePrivacyProFeatureEnabledPixel()
self.reportAdAttribution()
}

if appIsLaunching {
Expand Down
Loading
Loading