Skip to content

Commit

Permalink
experiment pixels (#3669)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204186595873227/1208775176602791/f
Tech Design URL:
https://app.asana.com/0/1204186595873227/1208682592686299
CC:

**Description**: Implements PixelExperimentKit to provide Experiment
metrics API to clients and fires pixel for preset experiment metrics
  • Loading branch information
SabrinaTardio authored Dec 5, 2024
1 parent abdc3c1 commit f0d0107
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 13 deletions.
10 changes: 10 additions & 0 deletions Core/StatisticsLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import Common
import Foundation
import BrowserServicesKit
import Networking
import PixelKit
import PixelExperimentKit
import os.log

public class StatisticsLoader {
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */,
Expand All @@ -3152,7 +3155,6 @@
F4D7F634298C00C3006C3AE9 /* FindInPageIOSJSSupport in Frameworks */,
9F96F73B2C9144D5009E45D5 /* Onboarding in Frameworks */,
85D598872927F84C00FA3B1B /* Crashes in Frameworks */,
D664C7DD2B28A02800CBFA76 /* StoreKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -7014,6 +7018,8 @@
CB6D8E972C80A9B100D0E772 /* SpecialErrorPages */,
CB6CC7E32CD2529000320907 /* BrokenSitePrompt */,
CBECDB6E2CD3DFBE005B8B87 /* PageRefreshMonitor */,
56D779392CFFC7E800B619EF /* PixelExperimentKit */,
56D7793B2CFFC7E800B619EF /* PixelKit */,
);
productName = Core;
productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */;
Expand Down Expand Up @@ -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" */ = {
Expand Down Expand Up @@ -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" */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
{
Expand Down
29 changes: 29 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import RemoteMessaging
import SyncDataProviders
import Subscription
import NetworkProtection
import PixelKit
import PixelExperimentKit
import WebKit
import os.log

Expand Down Expand Up @@ -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(
Expand Down
15 changes: 8 additions & 7 deletions DuckDuckGo/AppDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import Common
import NetworkProtection
import RemoteMessaging
import PageRefreshMonitor
import PixelKit
import PixelExperimentKit

protocol DependencyProvider {

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -93,13 +93,14 @@ final class AppDependencyProvider: DependencyProvider {
let persistentPixel: PersistentPixelFiring = PersistentPixel()

private init() {
let featureFlaggerOverrides = FeatureFlagLocalOverrides(keyValueStore: UserDefaults(suiteName: FeatureFlag.localOverrideStoreName)!,
actionHandler: FeatureFlagOverridesPublishingHandler<FeatureFlag>()
)
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<FeatureFlag>()
),
experimentManager: ExperimentCohortsManager(store: ExperimentsDataStore()),
localOverrides: featureFlaggerOverrides,
experimentManager: experimentManager,
for: FeatureFlag.self)

configurationManager = ConfigurationManager(store: configurationStore)
Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGoTests/StatisticsLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}

Expand All @@ -62,6 +66,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser)
XCTAssertTrue(fireAppRetentionExperimentPixelsCalled)
}

func testWhenReturnUser_ThenSegmentationIncludesCorrectVariant() {
Expand All @@ -76,6 +81,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertTrue(mockUsageSegmentation.atbs[0].installAtb.isReturningUser)
XCTAssertTrue(fireSearchExperimentPixelsCalled)
}

func testWhenSearchRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() {
Expand All @@ -87,6 +93,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty)
XCTAssertTrue(fireSearchExperimentPixelsCalled)
}

func testWhenAppRefreshHappensButNotInstalled_ThenRetentionSegmentationNotified() {
Expand All @@ -98,6 +105,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty)
XCTAssertTrue(fireAppRetentionExperimentPixelsCalled)
}

func testWhenStatisticsInstalled_ThenRetentionSegmentationNotNotified() {
Expand All @@ -122,6 +130,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty)
XCTAssertTrue(fireAppRetentionExperimentPixelsCalled)
}

func testWhenSearchRetentionRefreshHappens_ThenRetentionSegmentationNotified() {
Expand All @@ -135,6 +144,7 @@ class StatisticsLoaderTests: XCTestCase {
}
wait(for: [testExpectation], timeout: 5.0)
XCTAssertFalse(mockUsageSegmentation.atbs.isEmpty)
XCTAssertTrue(self.fireSearchExperimentPixelsCalled)
}

func testWhenSearchRefreshHasSuccessfulUpdateAtbRequestThenSearchRetentionAtbUpdated() {
Expand All @@ -151,6 +161,7 @@ class StatisticsLoaderTests: XCTestCase {
}

waitForExpectations(timeout: 5, handler: nil)
XCTAssertTrue(self.fireSearchExperimentPixelsCalled)
}

func testWhenAppRefreshHasSuccessfulUpdateAtbRequestThenAppRetentionAtbUpdated() {
Expand All @@ -167,6 +178,7 @@ class StatisticsLoaderTests: XCTestCase {
}

waitForExpectations(timeout: 5, handler: nil)
XCTAssertTrue(self.fireAppRetentionExperimentPixelsCalled)
}

func testWhenLoadHasSuccessfulAtbAndExtiRequestsThenStoreUpdatedWithVariant() {
Expand Down
33 changes: 32 additions & 1 deletion IntegrationTests/AtbServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import XCTest
@testable import Core
@testable import BrowserServicesKit
import Combine
import PixelKit

class AtbServerTests: XCTestCase {

Expand All @@ -32,7 +34,7 @@ class AtbServerTests: XCTestCase {

override func setUp() {
super.setUp()

PixelKit.configureExperimentKit(featureFlagger: MockFeatureFlagger())
store = MockStatisticsStore()
loader = StatisticsLoader(statisticsStore: store, inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring())

Expand Down Expand Up @@ -136,3 +138,32 @@ private struct MockStatisticsStoreInconsistencyMonitoring: StatisticsStoreIncons

}
}

class MockFeatureFlagger: FeatureFlagger {
func isFeatureOn<Flag>(for featureFlag: Flag, allowOverride: Bool) -> Bool where Flag: FeatureFlagDescribing {
return false
}

var internalUserDecider: any InternalUserDecider = MockInteranlUserDecider()

var localOverrides: (any BrowserServicesKit.FeatureFlagLocalOverriding)?

func getCohortIfEnabled<Flag>(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing {
return nil
}

func getAllActiveExperiments() -> Experiments {
return [:]
}

}

class MockInteranlUserDecider: InternalUserDecider {
var isInternalUser: Bool = false

var isInternalUserPublisher: AnyPublisher<Bool, Never> = Just(false).eraseToAnyPublisher()

func markUserAsInternalIfNeeded(forUrl url: URL?, response: HTTPURLResponse?) -> Bool {
return false
}
}

0 comments on commit f0d0107

Please sign in to comment.