Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into alex/malware-protecti…
Browse files Browse the repository at this point in the history
…on-6
  • Loading branch information
mallexxx committed Dec 3, 2024
2 parents fcbe2c2 + befc1f1 commit 30bc650
Show file tree
Hide file tree
Showing 66 changed files with 4,579 additions and 510 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PrivacyStats"
BuildableName = "PrivacyStats"
BlueprintName = "PrivacyStats"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
Expand Down Expand Up @@ -818,6 +832,16 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "PrivacyStatsTests"
BuildableName = "PrivacyStatsTests"
BlueprintName = "PrivacyStatsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
8 changes: 4 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
"revision" : "dfef00ef77f5181d1d8a4f7cc88f7b7c0514dd34",
"version" : "6.39.0"
"revision" : "c4bb146afdf0c7a93fb9a7d95b1cb255708a470d",
"version" : "6.41.0"
}
},
{
Expand Down Expand Up @@ -50,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/privacy-dashboard",
"state" : {
"revision" : "757bbbae1e2afbb421caee9bfca04ee5c56c3af8",
"version" : "7.2.0"
"revision" : "49db79829dcb166b3524afdbc1c680890452ce1c",
"version" : "7.2.1"
}
},
{
Expand Down
26 changes: 24 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ let package = Package(
.library(name: "Onboarding", targets: ["Onboarding"]),
.library(name: "BrokenSitePrompt", targets: ["BrokenSitePrompt"]),
.library(name: "PageRefreshMonitor", targets: ["PageRefreshMonitor"]),
.library(name: "PrivacyStats", targets: ["PrivacyStats"]),
],
dependencies: [
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "15.1.0"),
.package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.4.2"),
.package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.3.0"),
.package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "6.39.0"),
.package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "7.2.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "6.41.0"),
.package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "7.2.1"),
.package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"),
.package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"),
.package(url: "https://github.com/1024jp/GzipSwift.git", exact: "6.0.1"),
Expand Down Expand Up @@ -450,6 +451,20 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug))
]
),
.target(
name: "PrivacyStats",
dependencies: [
"Common",
"Persistence",
"TrackerRadarKit"
],
resources: [
.process("PrivacyStats.xcdatamodeld")
],
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
),

// MARK: - Test Targets
.testTarget(
Expand Down Expand Up @@ -686,6 +701,13 @@ let package = Package(
"PageRefreshMonitor"
]
),
.testTarget(
name: "PrivacyStatsTests",
dependencies: [
"PrivacyStats",
"TestUtils",
]
),
],
cxxLanguageStandard: .cxx11
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//
// ExperimentCohortsManager.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 CohortID = String
public typealias SubfeatureID = String
public typealias ParentFeatureID = String
public typealias Experiments = [String: ExperimentData]

public struct ExperimentSubfeature {
let parentID: ParentFeatureID
let subfeatureID: SubfeatureID
let cohorts: [PrivacyConfigurationData.Cohort]
}

public struct ExperimentData: Codable, Equatable {
public let parentID: ParentFeatureID
public let cohortID: CohortID
public let enrollmentDate: Date
}

public protocol ExperimentCohortsManaging {
/// Retrieves all the experiments a user is enrolled in
var experiments: Experiments? { get }

/// Resolves the cohort for a given experiment subfeature.
///
/// This method determines whether the user is currently assigned to a valid cohort
/// for the specified experiment. If the assigned cohort is valid (i.e., it matches
/// one of the experiment's defined cohorts), the method returns the assigned cohort.
/// Otherwise, the invalid cohort is removed, and a new cohort is assigned if
/// `allowCohortReassignment` is `true`.
///
/// - Parameters:
/// - experiment: The `ExperimentSubfeature` representing the experiment and its associated cohorts.
/// - allowCohortReassignment: A Boolean value indicating whether cohort assignment is allowed
/// if the user is not already assigned to a valid cohort.
///
/// - Returns: The valid `CohortID` assigned to the user for the experiment, or `nil`
/// if no valid cohort exists and `allowCohortReassignment` is `false`.
///
/// - Behavior:
/// 1. Retrieves the currently assigned cohort for the experiment using the `subfeatureID`.
/// 2. Validates if the assigned cohort exists within the experiment's cohort list:
/// - If valid, the assigned cohort is returned.
/// - If invalid, the cohort is removed from storage.
/// 3. If cohort assignment is enabled (`allowCohortReassignment` is `true`), a new cohort
/// is assigned based on the experiment's cohort weights and saved in storage.
/// - Cohort assignment is probabilistic, determined by the cohort weights.
///
func resolveCohort(for experiment: ExperimentSubfeature, allowCohortReassignment: Bool) -> CohortID?
}

public class ExperimentCohortsManager: ExperimentCohortsManaging {

private var store: ExperimentsDataStoring
private let randomizer: (Range<Double>) -> Double
private let queue = DispatchQueue(label: "com.ExperimentCohortsManager.queue")

public var experiments: Experiments? {
get {
queue.sync {
store.experiments
}
}
}

public init(store: ExperimentsDataStoring = ExperimentsDataStore(), randomizer: @escaping (Range<Double>) -> Double = Double.random(in:)) {
self.store = store
self.randomizer = randomizer
}

public func resolveCohort(for experiment: ExperimentSubfeature, allowCohortReassignment: Bool) -> CohortID? {
queue.sync {
let assignedCohort = cohort(for: experiment.subfeatureID)
if experiment.cohorts.contains(where: { $0.name == assignedCohort }) {
return assignedCohort
}
removeCohort(from: experiment.subfeatureID)
return allowCohortReassignment ? assignCohort(to: experiment) : nil
}
}
}

// MARK: Helper functions
extension ExperimentCohortsManager {

private func assignCohort(to subfeature: ExperimentSubfeature) -> CohortID? {
let cohorts = subfeature.cohorts
let totalWeight = cohorts.map(\.weight).reduce(0, +)
guard totalWeight > 0 else { return nil }

let randomValue = randomizer(0..<Double(totalWeight))
var cumulativeWeight = 0.0

for cohort in cohorts {
cumulativeWeight += Double(cohort.weight)
if randomValue < cumulativeWeight {
saveCohort(cohort.name, in: subfeature.subfeatureID, parentID: subfeature.parentID)
return cohort.name
}
}
return nil
}

func cohort(for subfeatureID: SubfeatureID) -> CohortID? {
guard let experiments = store.experiments else { return nil }
return experiments[subfeatureID]?.cohortID
}

private func enrollmentDate(for subfeatureID: SubfeatureID) -> Date? {
guard let experiments = store.experiments else { return nil }
return experiments[subfeatureID]?.enrollmentDate
}

private func removeCohort(from subfeatureID: SubfeatureID) {
guard var experiments = store.experiments else { return }
experiments.removeValue(forKey: subfeatureID)
store.experiments = experiments
}

private func saveCohort(_ cohort: CohortID, in experimentID: SubfeatureID, parentID: ParentFeatureID) {
var experiments = store.experiments ?? Experiments()
let experimentData = ExperimentData(parentID: parentID, cohortID: cohort, enrollmentDate: Date())
experiments[experimentID] = experimentData
store.experiments = experiments
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@

import Foundation

protocol ExperimentsDataStoring {
public protocol ExperimentsDataStoring {
var experiments: Experiments? { get set }
}

protocol LocalDataStoring {
public protocol LocalDataStoring {
func data(forKey defaultName: String) -> Data?
func set(_ value: Any?, forKey defaultName: String)
}

struct ExperimentsDataStore: ExperimentsDataStoring {
public struct ExperimentsDataStore: ExperimentsDataStoring {

private enum Constants {
static let experimentsDataKey = "ExperimentsData"
Expand All @@ -36,13 +36,13 @@ struct ExperimentsDataStore: ExperimentsDataStoring {
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()

init(localDataStoring: LocalDataStoring = UserDefaults.standard) {
public init(localDataStoring: LocalDataStoring = UserDefaults.standard) {
self.localDataStoring = localDataStoring
encoder.dateEncodingStrategy = .secondsSince1970
decoder.dateDecodingStrategy = .secondsSince1970
}

var experiments: Experiments? {
public var experiments: Experiments? {
get {
guard let savedData = localDataStoring.data(forKey: Constants.experimentsDataKey) else { return nil }
return try? decoder.decode(Experiments.self, from: savedData)
Expand Down
Loading

0 comments on commit 30bc650

Please sign in to comment.