Skip to content

Commit

Permalink
experiment default metrics pixels (#1107)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204186595873227/1208775176602791/f
iOS PR:  in prep
macOS PR:  duckduckgo/macos-browser#3622
What kind of version bump will this require?: N/A

Tech Design URL:
https://app.asana.com/0/1204186595873227/1208682592686299


**Description**: Implements PixelExperimentKit to provide Experiment
metrics API to clients
  • Loading branch information
SabrinaTardio authored Dec 5, 2024
1 parent f0755fb commit e5d390c
Show file tree
Hide file tree
Showing 14 changed files with 1,238 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PixelExperimentKit"
BuildableName = "PixelExperimentKit"
BlueprintName = "PixelExperimentKit"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down Expand Up @@ -842,6 +856,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PixelExperimentKitTests"
BuildableName = "PixelExperimentKitTests"
BlueprintName = "PixelExperimentKitTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
20 changes: 20 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let package = Package(
.library(name: "DuckPlayer", targets: ["DuckPlayer"]),
.library(name: "MaliciousSiteProtection", targets: ["MaliciousSiteProtection"]),
.library(name: "Onboarding", targets: ["Onboarding"]),
.library(name: "PixelExperimentKit", targets: ["PixelExperimentKit"]),
.library(name: "BrokenSitePrompt", targets: ["BrokenSitePrompt"]),
.library(name: "PageRefreshMonitor", targets: ["PageRefreshMonitor"]),
.library(name: "PrivacyStats", targets: ["PrivacyStats"]),
Expand Down Expand Up @@ -433,6 +434,19 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "PixelExperimentKit",
dependencies: [
"PixelKit",
"BrowserServicesKit"
],
resources: [
.process("Resources")
],
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "BrokenSitePrompt",
dependencies: [
Expand Down Expand Up @@ -683,6 +697,12 @@ let package = Package(
"Onboarding"
]
),
.testTarget(
name: "PixelExperimentKitTests",
dependencies: [
"PixelExperimentKit"
]
),
.testTarget(
name: "SpecialErrorPagesTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class ExperimentCohortsManager: ExperimentCohortsManaging {
private var store: ExperimentsDataStoring
private let randomizer: (Range<Double>) -> Double
private let queue = DispatchQueue(label: "com.ExperimentCohortsManager.queue")
private let fireCohortAssigned: (_ subfeatureID: SubfeatureID, _ experiment: ExperimentData) -> Void

public var experiments: Experiments? {
get {
Expand All @@ -81,9 +82,11 @@ public class ExperimentCohortsManager: ExperimentCohortsManaging {
}
}

public init(store: ExperimentsDataStoring = ExperimentsDataStore(), randomizer: @escaping (Range<Double>) -> Double = Double.random(in:)) {
public init(store: ExperimentsDataStoring = ExperimentsDataStore(), randomizer: @escaping (Range<Double>) -> Double = Double.random(in:),
fireCohortAssigned: @escaping (_ subfeatureID: SubfeatureID, _ experiment: ExperimentData) -> Void) {
self.store = store
self.randomizer = randomizer
self.fireCohortAssigned = fireCohortAssigned
}

public func resolveCohort(for experiment: ExperimentSubfeature, allowCohortReassignment: Bool) -> CohortID? {
Expand Down Expand Up @@ -113,6 +116,7 @@ extension ExperimentCohortsManager {
cumulativeWeight += Double(cohort.weight)
if randomValue < cumulativeWeight {
saveCohort(cohort.name, in: subfeature.subfeatureID, parentID: subfeature.parentID)
fireCohortAssigned(subfeature.subfeatureID, ExperimentData(parentID: subfeature.parentID, cohortID: cohort.name, enrollmentDate: Date()))
return cohort.name
}
}
Expand Down
80 changes: 80 additions & 0 deletions Sources/PixelExperimentKit/ExperimentEventTracker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// ExperimentEventTracker.swift
//
// 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

public typealias ThresholdCheckResult = Bool
public typealias ExprimentPixelNameAndParameters = String
public typealias NumberOfActions = Int

public protocol ExperimentActionPixelStore {
func removeObject(forKey defaultName: String)
func integer(forKey defaultName: String) -> Int
func set(_ value: Int, forKey defaultName: String)
}

public protocol ExperimentEventTracking {
/// Increments the count for a given event key and checks if the threshold has been exceeded.
///
/// This method performs the following actions:
/// 1. If the `isInWindow` parameter is `false`, it removes the stored count for the key and returns `false`.
/// 2. If `isInWindow` is `true`, it increments the count for the key.
/// 3. If the updated count meets or exceeds the specified `threshold`, the stored count is removed, and the method returns `true`.
/// 4. If the updated count does not meet the threshold, it updates the count and returns `false`.
///
/// - Parameters:
/// - key: The key used to store and retrieve the count.
/// - threshold: The count threshold that triggers a return of `true`.
/// - isInWindow: A flag indicating if the count should be considered (e.g., within a time window).
/// - Returns: `true` if the threshold is exceeded and the count is reset, otherwise `false`.
func incrementAndCheckThreshold(forKey key: ExprimentPixelNameAndParameters, threshold: NumberOfActions, isInWindow: Bool) -> ThresholdCheckResult
}

public struct ExperimentEventTracker: ExperimentEventTracking {
private let store: ExperimentActionPixelStore
private let syncQueue = DispatchQueue(label: "com.pixelkit.experimentActionSyncQueue")

public init(store: ExperimentActionPixelStore = UserDefaults.standard) {
self.store = store
}

public func incrementAndCheckThreshold(forKey key: ExprimentPixelNameAndParameters, threshold: NumberOfActions, isInWindow: Bool) -> ThresholdCheckResult {
syncQueue.sync {
// Remove the key if is not in window
guard isInWindow else {
store.removeObject(forKey: key)
return false
}

// Increment the current count
let currentCount = store.integer(forKey: key)
let newCount = currentCount + 1
store.set(newCount, forKey: key)

// Check if the threshold is exceeded
if newCount >= threshold {
store.removeObject(forKey: key)
return true
}
return false
}
}

}

extension UserDefaults: ExperimentActionPixelStore {}
Loading

0 comments on commit e5d390c

Please sign in to comment.