diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5cd4768fef..ae1c0a4b65 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1689,6 +1689,8 @@ 562984712AC469E400AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; 56406D4B2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */; }; 56406D4C2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */; }; + 5641734B2CFE168700F4B716 /* PixelExperimentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5641734A2CFE168700F4B716 /* PixelExperimentKit */; }; + 5641734D2CFE169400F4B716 /* PixelExperimentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5641734C2CFE169400F4B716 /* PixelExperimentKit */; }; 56534DED29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */; }; 56534DEE29DF252C00121467 /* CapturingDefaultBrowserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */; }; 565E46E02B2725DD0013AC2A /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; @@ -5083,6 +5085,7 @@ 567A23C52C7F75BB0010F66C /* SpecialErrorPages in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, + 5641734D2CFE169400F4B716 /* PixelExperimentKit in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, 560EB9372C78974C0080DBC8 /* Onboarding in Frameworks */, @@ -5317,6 +5320,7 @@ CD3301242C8870DF009AA127 /* MaliciousSiteProtection in Frameworks */, C18BF9CC2C73678500ED6B8A /* Freemium in Frameworks */, F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */, + 5641734B2CFE168700F4B716 /* PixelExperimentKit in Frameworks */, 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, @@ -10034,6 +10038,7 @@ CBECDB8D2CDBD62C005B8B87 /* PageRefreshMonitor */, CBECDB8F2CDBD631005B8B87 /* BrokenSitePrompt */, 37DF37062CF38B9F005ED34B /* PrivacyStats */, + 5641734C2CFE169400F4B716 /* PixelExperimentKit */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -10514,6 +10519,7 @@ CBECDB892CDBD616005B8B87 /* PageRefreshMonitor */, CBECDB8B2CDBD61C005B8B87 /* BrokenSitePrompt */, 37DF37042CF38B96005ED34B /* PrivacyStats */, + 5641734A2CFE168700F4B716 /* PixelExperimentKit */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -15365,7 +15371,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 217.0.2; + version = 218.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -15820,6 +15826,14 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Onboarding; }; + 5641734A2CFE168700F4B716 /* PixelExperimentKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelExperimentKit; + }; + 5641734C2CFE169400F4B716 /* PixelExperimentKit */ = { + isa = XCSwiftPackageProductDependency; + productName = PixelExperimentKit; + }; 567A23C02C7F71570010F66C /* SpecialErrorPages */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5e825895d4..7bd364813b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "f0755fbb3309c93c8490dc8bbdfb7e2e7613bef6", - "version" : "217.0.2" + "revision" : "e5d390c8559fbe7b1ca67fd3982c91bcc0437d60", + "version" : "218.0.0" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm", + "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 9316721cce..3d79b94ed3 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -31,6 +31,7 @@ import MetricKit import Networking import Persistence import PixelKit +import PixelExperimentKit import ServiceManagement import SyncDataProviders import UserNotifications @@ -282,7 +283,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { keyValueStore: UserDefaults.appConfiguration, actionHandler: FeatureFlagOverridesPublishingHandler() ), - experimentManager: ExperimentCohortsManager(store: ExperimentsDataStore()), + experimentManager: ExperimentCohortsManager(store: ExperimentsDataStore(), fireCohortAssigned: PixelKit.fireExperimentEnrollmentPixel(subfeatureID:experiment:)), for: FeatureFlag.self ) @@ -331,6 +332,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #else privacyStats = PrivacyStats(databaseProvider: PrivacyStatsDatabase()) #endif + PixelKit.configureExperimentKit(featureFlagger: featureFlagger, eventTracker: ExperimentEventTracker(store: UserDefaults.appConfiguration)) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift index 3c48f090b6..eeacd17952 100644 --- a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift +++ b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift @@ -119,7 +119,7 @@ extension HomePage.Views { .onAppear { if featureType == .dock { PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, - frequency: .unique, + frequency: .uniqueByName, includeAppVersionParameter: false) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index a35f9a6d29..9c5606b3df 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -654,7 +654,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr PixelKit.fire( NetworkProtectionPixelEvent.networkProtectionNewUser, - frequency: .unique, + frequency: .uniqueByName, includeAppVersionParameter: true) { [weak self] fired, error in guard let self, error == nil, fired else { return } self.defaults.vpnFirstEnabled = PixelKit.pixelLastFireDate(event: NetworkProtectionPixelEvent.networkProtectionNewUser) diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index d508f01d2c..030e5e1bda 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -78,7 +78,7 @@ extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding return } PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, - frequency: .unique, + frequency: .uniqueByName, includeAppVersionParameter: false) } } diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingPixelReporter.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingPixelReporter.swift index 24f6d0570e..b800061a1f 100644 --- a/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingPixelReporter.swift +++ b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingPixelReporter.swift @@ -57,27 +57,27 @@ final class OnboardingPixelReporter: OnboardingSearchSuggestionsPixelReporting, } func trackSiteSuggetionOptionTapped() { - fire(ContextualOnboardingPixel.siteSuggetionOptionTapped, .unique) + fire(ContextualOnboardingPixel.siteSuggetionOptionTapped, .uniqueByName) } func trackSearchSuggetionOptionTapped() { - fire(ContextualOnboardingPixel.searchSuggetionOptionTapped, .unique) + fire(ContextualOnboardingPixel.searchSuggetionOptionTapped, .uniqueByName) } } extension OnboardingPixelReporter: OnboardingAddressBarReporting { func trackPrivacyDashboardOpened() { if onboardingStateProvider.state != .onboardingCompleted { - fire(ContextualOnboardingPixel.onboardingPrivacyDashboardOpened, .unique) + fire(ContextualOnboardingPixel.onboardingPrivacyDashboardOpened, .uniqueByName) } } func trackAddressBarTypedIn() { if onboardingStateProvider.state == .showTryASearch { - fire(ContextualOnboardingPixel.onboardingSearchCustom, .unique) + fire(ContextualOnboardingPixel.onboardingSearchCustom, .uniqueByName) } if onboardingStateProvider.state == .showTryASite { - fire(ContextualOnboardingPixel.onboardingVisitSiteCustom, .unique) + fire(ContextualOnboardingPixel.onboardingVisitSiteCustom, .uniqueByName) } } @@ -85,7 +85,7 @@ extension OnboardingPixelReporter: OnboardingAddressBarReporting { let key = "onboarding.website-visited" let siteVisited = userDefaults.bool(forKey: key) if siteVisited { - fire(ContextualOnboardingPixel.secondSiteVisited, .unique) + fire(ContextualOnboardingPixel.secondSiteVisited, .uniqueByName) } else { userDefaults.set(true, forKey: key) } @@ -95,21 +95,21 @@ extension OnboardingPixelReporter: OnboardingAddressBarReporting { extension OnboardingPixelReporter: OnboardingFireReporting { func trackFireButtonPressed() { if onboardingStateProvider.state != .onboardingCompleted { - fire(ContextualOnboardingPixel.onboardingFireButtonPressed, .unique) + fire(ContextualOnboardingPixel.onboardingFireButtonPressed, .uniqueByName) } } } extension OnboardingPixelReporter: OnboardingDialogsReporting { func trackLastDialogShown() { - fire(ContextualOnboardingPixel.onboardingFinished, .unique) + fire(ContextualOnboardingPixel.onboardingFinished, .uniqueByName) } func trackFireButtonSkipped() { - fire(ContextualOnboardingPixel.onboardingFireButtonPromptSkipPressed, .unique) + fire(ContextualOnboardingPixel.onboardingFireButtonPromptSkipPressed, .uniqueByName) } func trackFireButtonTryIt() { - fire(ContextualOnboardingPixel.onboardingFireButtonTryItPressed, .unique) + fire(ContextualOnboardingPixel.onboardingFireButtonTryItPressed, .uniqueByName) } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index b9f186fc68..c1552f3cfc 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -163,7 +163,7 @@ enum Preferences { case .activateAddEmailClick: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseEmailStart, frequency: .legacyDailyAndCount) case .postSubscriptionAddEmailClick: - PixelKit.fire(PrivacyProPixel.privacyProWelcomeAddDevice, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProWelcomeAddDevice, frequency: .uniqueByName) case .restorePurchaseStoreClick: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreStart, frequency: .legacyDailyAndCount) case .addDeviceEnterEmail: diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index b63adf3d0d..a4e6f1a4b2 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -21,6 +21,7 @@ import Foundation import BrowserServicesKit import Networking import PixelKit +import PixelExperimentKit import os.log final class StatisticsLoader { @@ -34,15 +35,21 @@ final class StatisticsLoader { private let attributionPixelHandler: InstallationAttributionsPixelHandler private let parser = AtbParser() private var isAppRetentionRequestInProgress = false + private let fireSearchExperimentPixels: () -> Void + private let fireAppRetentionExperimentPixels: () -> Void init( statisticsStore: StatisticsStore = LocalStatisticsStore(), emailManager: EmailManager = EmailManager(), - attributionPixelHandler: InstallationAttributionsPixelHandler = AppInstallationAttributionPixelHandler() + attributionPixelHandler: InstallationAttributionsPixelHandler = AppInstallationAttributionPixelHandler(), + fireAppRetentionExperimentPixels: @escaping () -> Void = PixelKit.fireAppRetentionExperimentPixels, + fireSearchExperimentPixels: @escaping () -> Void = PixelKit.fireSearchExperimentPixels ) { self.statisticsStore = statisticsStore self.emailManager = emailManager self.attributionPixelHandler = attributionPixelHandler + self.fireSearchExperimentPixels = fireSearchExperimentPixels + self.fireAppRetentionExperimentPixels = fireAppRetentionExperimentPixels } func refreshRetentionAtb(isSearch: Bool, completion: @escaping Completion = {}) { @@ -57,13 +64,16 @@ final class StatisticsLoader { } PixelExperiment.fireSerpPixel() PixelExperiment.fireOnboardingSearchPerformed5to7Pixel() + self.fireSearchExperimentPixels() if NSApp.runType == .normal { self.fireDailyOsVersionCounterPixel() } self.fireDockPixel() } else if !self.statisticsStore.isAppRetentionFiredToday { self.refreshAppRetentionAtb(completion: completion) + self.fireAppRetentionExperimentPixels() } else { + self.fireAppRetentionExperimentPixels() completion() } } diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift index 29354841b7..8ade538671 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUseSubscriptionFeature.swift @@ -278,7 +278,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .legacyDailyAndCount) sendFreemiumSubscriptionPixelIfFreemiumActivated() saveSubscriptionUpgradeTimestampIfFreemiumActivated() - PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActivated, frequency: .uniqueByName) subscriptionSuccessPixelHandler.fireSuccessfulSubscriptionAttributionPixel() sendSubscriptionUpgradeFromFreemiumNotificationIfFreemiumActivated() await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: purchaseUpdate) @@ -354,14 +354,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { switch featureSelection.productFeature { case .networkProtection: - PixelKit.fire(PrivacyProPixel.privacyProWelcomeVPN, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProWelcomeVPN, frequency: .uniqueByName) notificationCenter.post(name: .ToggleNetworkProtectionInMainWindow, object: self, userInfo: nil) case .dataBrokerProtection: - PixelKit.fire(PrivacyProPixel.privacyProWelcomePersonalInformationRemoval, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProWelcomePersonalInformationRemoval, frequency: .uniqueByName) notificationCenter.post(name: .openPersonalInformationRemoval, object: self, userInfo: nil) await uiHandler.showTab(with: .dataBrokerProtection) case .identityTheftRestoration, .identityTheftRestorationGlobal: - PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .uniqueByName) let url = subscriptionManager.url(for: .identityTheftRestoration) await uiHandler.showTab(with: .identityTheftRestoration(url)) case .unknown: @@ -402,12 +402,12 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func subscriptionsAddEmailSuccess(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProAddEmailSuccess, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProAddEmailSuccess, frequency: .uniqueByName) return nil } func subscriptionsWelcomeFaqClicked(params: Any, original: WKScriptMessage) async -> Encodable? { - PixelKit.fire(PrivacyProPixel.privacyProWelcomeFAQClick, frequency: .unique) + PixelKit.fire(PrivacyProPixel.privacyProWelcomeFAQClick, frequency: .uniqueByName) return nil } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index a1c31873a7..35ceb38061 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "218.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/FreemiumDBPExperimentPixel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/FreemiumDBPExperimentPixel.swift index 490337ffae..fd5c8ef0b7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/FreemiumDBPExperimentPixel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/FreemiumDBPExperimentPixel.swift @@ -25,9 +25,9 @@ public class FreemiumDBPExperimentPixelHandler: EventMapping