Skip to content

Commit

Permalink
add tds experiment features (#1161)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1209114783599181/f
iOS PR: duckduckgo/iOS#3805
macOS PR: duckduckgo/macos-browser#3726
What kind of version bump will this require?: Major
  • Loading branch information
SabrinaTardio authored Jan 17, 2025
1 parent 0a526c5 commit 20e6eaf
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration {
return data.features[feature.rawValue]?.settings ?? [:]
}

public func settings(for subfeature: any PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? {
guard let subfeatureData = subfeatures(for: subfeature.parent)[subfeature.rawValue] else {
return nil
}
return subfeatureData.settings
}

public func userEnabledProtection(forDomain domain: String) {
let domainToRemove = locallyUnprotected.unprotectedDomains.first { unprotectedDomain in
unprotectedDomain.punycodeEncodedHostname.lowercased() == domain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,19 @@ public enum HTMLNewTabPageSubfeature: String, Equatable, PrivacySubfeature {
public var parent: PrivacyFeature { .htmlNewTabPage }
case isLaunched
}

public enum ContentBlockingSubfeature: String, Equatable, PrivacySubfeature {
public var parent: PrivacyFeature { .contentBlocking }
case tdsNextExperimentBaseline
case tdsNextExperimentFeb25
case tdsNextExperimentMar25
case tdsNextExperimentApr25
case tdsNextExperimentMay25
case tdsNextExperimentJun25
case tdsNextExperimentJul25
case tdsNextExperimentAug25
case tdsNextExperimentSep25
case tdsNextExperimentOct25
case tdsNextExperimentNov25
case tdsNextExperimentDec25
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ public protocol PrivacyConfiguration {
/// Returns settings for a specified feature.
func settings(for feature: PrivacyFeature) -> PrivacyConfigurationData.PrivacyFeature.FeatureSettings

/// Returns settings for a specified subfeature.
func settings(for subfeature: any PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings?

/// Removes given domain from locally unprotected list.
func userEnabledProtection(forDomain: String)
/// Adds given domain to locally unprotected list.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public struct PrivacyConfigurationData {
public typealias FeatureSettings = [String: Any]
public typealias Features = [String: Feature]
public typealias FeatureSupportedVersion = String
public typealias SubfeatureSettings = String

enum CodingKeys: String {
case state
Expand Down Expand Up @@ -191,7 +192,7 @@ public struct PrivacyConfigurationData {
public let rollout: Rollout?
public let cohorts: [Cohort]?
public let targets: [Target]?
public let settings: String?
public let settings: SubfeatureSettings?

public init?(json: [String: Any]) {
guard let state = json[CodingKeys.state.rawValue] as? String else {
Expand Down
131 changes: 131 additions & 0 deletions Sources/Configuration/TrackerDataURLOverrider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//
// TrackerDataURLOverrider.swift
//
// Copyright © 2025 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
import BrowserServicesKit
import os.log

public protocol TrackerDataURLProviding {
var trackerDataURL: URL? { get }
}

public final class TrackerDataURLOverrider: TrackerDataURLProviding {

var privacyConfigurationManager: PrivacyConfigurationManaging
var featureFlagger: FeatureFlagger

public enum Constants {
public static let baseTdsURLString = "https://staticcdn.duckduckgo.com/trackerblocking/"
}

public init (privacyConfigurationManager: PrivacyConfigurationManaging,
featureFlagger: FeatureFlagger) {
self.privacyConfigurationManager = privacyConfigurationManager
self.featureFlagger = featureFlagger
}

public var trackerDataURL: URL? {
for experimentType in TdsExperimentType.allCases {
if let cohort = featureFlagger.getCohortIfEnabled(for: experimentType.experiment) as? TdsNextExperimentFlag.Cohort,
let url = trackerDataURL(for: experimentType.subfeature, cohort: cohort) {
return url
}
}
return nil
}

private func trackerDataURL(for subfeature: any PrivacySubfeature, cohort: TdsNextExperimentFlag.Cohort) -> URL? {
guard let settings = privacyConfigurationManager.privacyConfig.settings(for: subfeature),
let jsonData = settings.data(using: .utf8) else { return nil }
do {
if let settingsDict = try JSONSerialization.jsonObject(with: jsonData) as? [String: String],
let urlString = cohort == .control ? settingsDict["controlUrl"] : settingsDict["treatmentUrl"] {
return URL(string: Constants.baseTdsURLString + urlString)
}
} catch {
Logger.config.info("privacyConfiguration: Failed to parse subfeature settings JSON: \(error)")
}
return nil
}
}

public enum TdsExperimentType: Int, CaseIterable {
case baseline
case feb25
case mar25
case apr25
case may25
case jun25
case jul25
case aug25
case sep25
case oct25
case nov25
case dec25

public var experiment: any FeatureFlagExperimentDescribing {
TdsNextExperimentFlag(subfeature: self.subfeature)
}

public var subfeature: any PrivacySubfeature {
switch self {
case .baseline:
ContentBlockingSubfeature.tdsNextExperimentBaseline
case .feb25:
ContentBlockingSubfeature.tdsNextExperimentFeb25
case .mar25:
ContentBlockingSubfeature.tdsNextExperimentMar25
case .apr25:
ContentBlockingSubfeature.tdsNextExperimentApr25
case .may25:
ContentBlockingSubfeature.tdsNextExperimentMay25
case .jun25:
ContentBlockingSubfeature.tdsNextExperimentJun25
case .jul25:
ContentBlockingSubfeature.tdsNextExperimentJul25
case .aug25:
ContentBlockingSubfeature.tdsNextExperimentAug25
case .sep25:
ContentBlockingSubfeature.tdsNextExperimentSep25
case .oct25:
ContentBlockingSubfeature.tdsNextExperimentOct25
case .nov25:
ContentBlockingSubfeature.tdsNextExperimentNov25
case .dec25:
ContentBlockingSubfeature.tdsNextExperimentDec25
}
}

}

public struct TdsNextExperimentFlag: FeatureFlagExperimentDescribing {
public var rawValue: String
public var source: FeatureFlagSource

public init(subfeature: any PrivacySubfeature) {
self.source = .remoteReleasable(.subfeature(subfeature))
self.rawValue = subfeature.rawValue
}

public typealias CohortType = Cohort

public enum Cohort: String, FlagCohort {
case control
case treatment
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ class PrivacyConfigurationMock: PrivacyConfiguration {
return settings[feature] ?? [:]
}

func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? {
return nil
}

var userUnprotected = Set<String>()
func userEnabledProtection(forDomain domain: String) {
userUnprotected.remove(domain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ class PrivacyConfigurationMock: PrivacyConfiguration {
return nil
}

func settings(for subfeature: any PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? {
return nil
}

var identifier: String = "abcd"
var version: String? = "123456789"
var userUnprotectedDomains: [String] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ class MockPrivacyConfiguration: PrivacyConfiguration {
return nil
}

func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? {
return nil
}

var identifier: String = "abcd"
var version: String? = "123456789"
var userUnprotectedDomains: [String] = []
Expand Down
Loading

0 comments on commit 20e6eaf

Please sign in to comment.