diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index 7d60430add..166e8e5014 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -21,6 +21,8 @@ import Common import Foundation import BrowserServicesKit import Networking +import PixelKit +import PixelExperimentKit import os.log public class StatisticsLoader { @@ -35,17 +37,23 @@ public class StatisticsLoader { private let parser = AtbParser() private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent) private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring + private let fireSearchExperimentPixels: () -> Void + private let fireAppRetentionExperimentPixels: () -> Void private let pixelFiring: PixelFiring.Type init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(), usageSegmentation: UsageSegmenting = UsageSegmentation(), inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor(), + fireAppRetentionExperimentPixels: @escaping () -> Void = PixelKit.fireAppRetentionExperimentPixels, + fireSearchExperimentPixels: @escaping () -> Void = PixelKit.fireSearchExperimentPixels, pixelFiring: PixelFiring.Type = Pixel.self) { self.statisticsStore = statisticsStore self.returnUserMeasurement = returnUserMeasurement self.usageSegmentation = usageSegmentation self.inconsistencyMonitoring = inconsistencyMonitoring + self.fireSearchExperimentPixels = fireSearchExperimentPixels + self.fireAppRetentionExperimentPixels = fireAppRetentionExperimentPixels self.pixelFiring = pixelFiring } @@ -125,6 +133,7 @@ public class StatisticsLoader { } public func refreshSearchRetentionAtb(completion: @escaping Completion = {}) { + fireSearchExperimentPixels() guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeSearchAtbURL() else { requestInstallStatistics { self.updateUsageSegmentationAfterInstall(activityType: .search) @@ -154,6 +163,7 @@ public class StatisticsLoader { } public func refreshAppRetentionAtb(completion: @escaping Completion = {}) { + fireAppRetentionExperimentPixels() guard let url = StatisticsDependentURLFactory(statisticsStore: statisticsStore).makeAppAtbURL() else { requestInstallStatistics { self.updateUsageSegmentationAfterInstall(activityType: .appUse) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index c1ff74b017..0d51c5c184 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -288,6 +288,9 @@ 56D060262C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060252C359D2E003BAEB5 /* ContextualOnboardingDialogs.swift */; }; 56D060282C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D060272C380D83003BAEB5 /* OnboardingSuggestedSearchesProvider.swift */; }; 56D0602D2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D0602C2C383FD2003BAEB5 /* OnboardingSuggestedSearchesProviderTests.swift */; }; + 56D7792C2CFF476800B619EF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; + 56D7793A2CFFC7E800B619EF /* PixelExperimentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 56D779392CFFC7E800B619EF /* PixelExperimentKit */; }; + 56D7793C2CFFC7E800B619EF /* PixelKit in Frameworks */ = {isa = PBXBuildFile; productRef = 56D7793B2CFFC7E800B619EF /* PixelKit */; }; 56D8556A2BEA9169009F9698 /* CurrentDateProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D855692BEA9169009F9698 /* CurrentDateProviding.swift */; }; 56D8556C2BEA91C4009F9698 /* SyncAlertsPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D8556B2BEA91C4009F9698 /* SyncAlertsPresenting.swift */; }; 6AC6DAB328804F97002723C0 /* BarsAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AC6DAB228804F97002723C0 /* BarsAnimator.swift */; }; @@ -1043,7 +1046,6 @@ D664C7C92B289AA200CBFA76 /* AsyncHeadlessWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7AF2B289AA000CBFA76 /* AsyncHeadlessWebView.swift */; }; D664C7CC2B289AA200CBFA76 /* SubscriptionPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B32B289AA000CBFA76 /* SubscriptionPagesUserScript.swift */; }; D664C7CE2B289AA200CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664C7B52B289AA000CBFA76 /* SubscriptionPagesUseSubscriptionFeature.swift */; }; - D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D664C7DC2B28A02800CBFA76 /* StoreKit.framework */; }; D668D9252B693778008E2FF2 /* SubscriptionITPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9242B693778008E2FF2 /* SubscriptionITPView.swift */; }; D668D9272B6937D2008E2FF2 /* SubscriptionITPViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9262B6937D2008E2FF2 /* SubscriptionITPViewModel.swift */; }; D668D9292B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = D668D9282B69681C008E2FF2 /* IdentityTheftRestorationPagesUserScript.swift */; }; @@ -3141,6 +3143,7 @@ 9F8FE9492BAE50E50071E372 /* Lottie in Frameworks */, 853273B624FFE0BB00E3C778 /* WidgetKit.framework in Frameworks */, 0238E44F29C0FAA100615E30 /* FindInPageIOSJSSupport in Frameworks */, + 56D7792C2CFF476800B619EF /* StoreKit.framework in Frameworks */, 3760DFED299315EF0045A446 /* Waitlist in Frameworks */, F1D43AFA2B99C1D300BAB743 /* BareBonesBrowserKit in Frameworks */, F143C2EB1E4A4CD400CFDE3A /* Core.framework in Frameworks */, @@ -3152,7 +3155,6 @@ F4D7F634298C00C3006C3AE9 /* FindInPageIOSJSSupport in Frameworks */, 9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */, 85D598872927F84C00FA3B1B /* Crashes in Frameworks */, - D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3260,7 +3262,9 @@ CBC83E3429B631780008E19C /* Configuration in Frameworks */, D61CDA182B7CF78300A0FBB9 /* ZIPFoundation in Frameworks */, CB6D8E982C80A9B100D0E772 /* SpecialErrorPages in Frameworks */, + 56D7793A2CFFC7E800B619EF /* PixelExperimentKit in Frameworks */, 851F74262B9A1BFD00747C42 /* Suggestions in Frameworks */, + 56D7793C2CFFC7E800B619EF /* PixelKit in Frameworks */, 98A16C2D28A11D6200A6C003 /* BrowserServicesKit in Frameworks */, 8599690F29D2F1C100DBF9FA /* DDGSync in Frameworks */, 851481882A600EFC00ABC65F /* RemoteMessaging in Frameworks */, @@ -7014,6 +7018,8 @@ CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */, CB6CC7E32CD2529000320907 /* BrokenSitePrompt */, CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */, + 56D779392CFFC7E800B619EF /* PixelExperimentKit */, + 56D7793B2CFFC7E800B619EF /* PixelKit */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -11358,7 +11364,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 217.0.2; + version = 218.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -11528,6 +11534,16 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Common; }; + 56D779392CFFC7E800B619EF /* PixelExperimentKit */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelExperimentKit; + }; + 56D7793B2CFFC7E800B619EF /* PixelKit */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PixelKit; + }; 851481872A600EFC00ABC65F /* RemoteMessaging */ = { isa = XCSwiftPackageProductDependency; package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2ef745a21a..7d8e4affb7 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" } }, { diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 1f6cb0cf60..0ec3c93bf1 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -36,6 +36,8 @@ import RemoteMessaging import SyncDataProviders import Subscription import NetworkProtection +import PixelKit +import PixelExperimentKit import WebKit import os.log @@ -294,6 +296,33 @@ import os.log ).wrappedValue ) ?? defaultEnvironment + var dryRun = false +#if DEBUG + dryRun = true +#endif + let isPhone = UIDevice.current.userInterfaceIdiom == .phone + let source = isPhone ? PixelKit.Source.iOS : PixelKit.Source.iPadOS + PixelKit.setUp(dryRun: dryRun, + appVersion: AppVersion.shared.versionNumber, + source: source.rawValue, + defaultHeaders: [:], + defaults: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { (pixelName: String, headers: [String: String], parameters: [String: String], _, _, onComplete: @escaping PixelKit.CompletionBlock) in + + let url = URL.pixelUrl(forPixelNamed: pixelName) + let apiHeaders = APIRequestV2.HeadersV2(additionalHeaders: headers) + let request = APIRequestV2(url: url, method: .get, queryItems: parameters, headers: apiHeaders) + Task { + do { + _ = try await DefaultAPIService().fetch(request: request) + onComplete(true, nil) + } catch { + onComplete(false, error) + } + } + } + PixelKit.configureExperimentKit(featureFlagger: AppDependencyProvider.shared.featureFlagger, + eventTracker: ExperimentEventTracker(store: UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults())) + let syncErrorHandler = SyncErrorHandler() syncDataProviders = SyncDataProviders( diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index 0ee74aa535..00b6f9fc29 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -27,6 +27,8 @@ import Common import NetworkProtection import RemoteMessaging import PageRefreshMonitor +import PixelKit +import PixelExperimentKit protocol DependencyProvider { @@ -58,10 +60,8 @@ protocol DependencyProvider { final class AppDependencyProvider: DependencyProvider { static var shared: DependencyProvider = AppDependencyProvider() - let appSettings: AppSettings = AppUserDefaults() let variantManager: VariantManager = DefaultVariantManager() - let internalUserDecider: InternalUserDecider = ContentBlocking.shared.privacyConfigurationManager.internalUserDecider let featureFlagger: FeatureFlagger @@ -93,13 +93,14 @@ final class AppDependencyProvider: DependencyProvider { let persistentPixel: PersistentPixelFiring = PersistentPixel() private init() { + let featureFlaggerOverrides = FeatureFlagLocalOverrides(keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!, + actionHandler: FeatureFlagOverridesPublishingHandler() + ) + let experimentManager = ExperimentCohortsManager(store: ExperimentsDataStore(), fireCohortAssigned: PixelKit.fireExperimentEnrollmentPixel(subfeatureID:experiment:)) featureFlagger = DefaultFeatureFlagger(internalUserDecider: internalUserDecider, privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, - localOverrides: FeatureFlagLocalOverrides( - keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!, - actionHandler: FeatureFlagOverridesPublishingHandler() - ), - experimentManager: ExperimentCohortsManager(store: ExperimentsDataStore()), + localOverrides: featureFlaggerOverrides, + experimentManager: experimentManager, for: FeatureFlag.self) configurationManager = ConfigurationManager(store: configurationStore) diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index 8fa235203c..f64d667df5 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -29,6 +29,8 @@ class StatisticsLoaderTests: XCTestCase { var mockUsageSegmentation: MockUsageSegmentation! var mockPixelFiring: PixelFiringMock.Type! var testee: StatisticsLoader! + private var fireAppRetentionExperimentPixelsCalled = false + private var fireSearchExperimentPixelsCalled = false override func setUpWithError() throws { try super.setUpWithError() @@ -41,6 +43,8 @@ class StatisticsLoaderTests: XCTestCase { testee = StatisticsLoader(statisticsStore: mockStatisticsStore, usageSegmentation: mockUsageSegmentation, inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring(), + fireAppRetentionExperimentPixels: { self.fireAppRetentionExperimentPixelsCalled = true }, + fireSearchExperimentPixels: { self.fireSearchExperimentPixelsCalled = true }, pixelFiring: mockPixelFiring) } @@ -62,6 +66,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + XCTAssertTrue(fireAppRetentionExperimentPixelsCalled) } func testWhenReturnUser_ThenSegmentationIncludesCorrectVariant() { @@ -76,6 +81,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser) + XCTAssertTrue(fireSearchExperimentPixelsCalled) } func testWhenSearchRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() { @@ -87,6 +93,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty) + XCTAssertTrue(fireSearchExperimentPixelsCalled) } func testWhenAppRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() { @@ -98,6 +105,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty) + XCTAssertTrue(fireAppRetentionExperimentPixelsCalled) } func testWhenStatisticsInstalled_ThenRetentionSegmentationNotNotified() { @@ -122,6 +130,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty) + XCTAssertTrue(fireAppRetentionExperimentPixelsCalled) } func testWhenSearchRetentionRefreshHappens_ThenRetentionSegmentationNotified() { @@ -135,6 +144,7 @@ class StatisticsLoaderTests: XCTestCase { } wait(for: [testExpectation], timeout: 5.0) XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty) + XCTAssertTrue(self.fireSearchExperimentPixelsCalled) } func testWhenSearchRefreshHasSuccessfulUpdateAtbRequestThenSearchRetentionAtbUpdated() { @@ -151,6 +161,7 @@ class StatisticsLoaderTests: XCTestCase { } waitForExpectations(timeout: 5, handler: nil) + XCTAssertTrue(self.fireSearchExperimentPixelsCalled) } func testWhenAppRefreshHasSuccessfulUpdateAtbRequestThenAppRetentionAtbUpdated() { @@ -167,6 +178,7 @@ class StatisticsLoaderTests: XCTestCase { } waitForExpectations(timeout: 5, handler: nil) + XCTAssertTrue(self.fireAppRetentionExperimentPixelsCalled) } func testWhenLoadHasSuccessfulAtbAndExtiRequestsThenStoreUpdatedWithVariant() { diff --git a/IntegrationTests/AtbServerTests.swift b/IntegrationTests/AtbServerTests.swift index 029e2bba34..624e66a3e4 100644 --- a/IntegrationTests/AtbServerTests.swift +++ b/IntegrationTests/AtbServerTests.swift @@ -20,6 +20,8 @@ import XCTest @testable import Core @testable import BrowserServicesKit +import Combine +import PixelKit class AtbServerTests: XCTestCase { @@ -32,7 +34,7 @@ class AtbServerTests: XCTestCase { override func setUp() { super.setUp() - + PixelKit.configureExperimentKit(featureFlagger: MockFeatureFlagger()) store = MockStatisticsStore() loader = StatisticsLoader(statisticsStore: store, inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) @@ -136,3 +138,32 @@ private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreIncons } } + +class MockFeatureFlagger: FeatureFlagger { + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool where Flag: FeatureFlagDescribing { + return false + } + + var internalUserDecider: any InternalUserDecider = MockInteranlUserDecider() + + var localOverrides: (any BrowserServicesKit.FeatureFlagLocalOverriding)? + + func getCohortIfEnabled(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing { + return nil + } + + func getAllActiveExperiments() -> Experiments { + return [:] + } + +} + +class MockInteranlUserDecider: InternalUserDecider { + var isInternalUser: Bool = false + + var isInternalUserPublisher: AnyPublisher = Just(false).eraseToAnyPublisher() + + func markUserAsInternalIfNeeded(forUrl url: URL?, response: HTTPURLResponse?) -> Bool { + return false + } +}