Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experiment default metrics pixels #1107

Merged
merged 57 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
216a00e
implement experiment manager
SabrinaTardio Nov 8, 2024
d8b385c
Merge branch 'main' into sabrina/add-experiment-manager
SabrinaTardio Nov 8, 2024
914a5ed
fix lint issues
SabrinaTardio Nov 8, 2024
93247a3
fix linting issue
SabrinaTardio Nov 8, 2024
5d530f2
wrap UserDefaults
SabrinaTardio Nov 8, 2024
2dcef1e
fix linting
SabrinaTardio Nov 8, 2024
5aec88d
refactor
SabrinaTardio Nov 8, 2024
b188d67
fix linting
SabrinaTardio Nov 8, 2024
1054923
address some comments
SabrinaTardio Nov 8, 2024
c05aa96
minor refactor
SabrinaTardio Nov 8, 2024
8387472
use Constants enum
SabrinaTardio Nov 8, 2024
e862ad8
add Targets and configurations
SabrinaTardio Nov 11, 2024
6090a15
fix linting issues
SabrinaTardio Nov 11, 2024
14f92e9
initial implementation
SabrinaTardio Nov 12, 2024
8bda08c
add tests
SabrinaTardio Nov 14, 2024
9a19db2
Merge branch 'main' into sabrina/add-experiment-logic
SabrinaTardio Nov 14, 2024
56c54a7
refactor
SabrinaTardio Nov 15, 2024
86b7c97
getAllActiveExperiments tests
SabrinaTardio Nov 15, 2024
2595fcf
sort linting issues
SabrinaTardio Nov 15, 2024
86d6f7b
fix linting
SabrinaTardio Nov 15, 2024
95c4470
don't break the API
SabrinaTardio Nov 15, 2024
c30f0b4
Merge branch 'main' into sabrina/add-experiment-logic
SabrinaTardio Nov 15, 2024
727e813
implement PixelExperimentKit
SabrinaTardio Nov 21, 2024
69dc2fd
implement framework
SabrinaTardio Nov 27, 2024
cfe3b13
clean up
SabrinaTardio Nov 27, 2024
1e426e2
Merge branch 'main' into sabrina/experiment-framework
SabrinaTardio Nov 27, 2024
26fa2ed
fix linting
SabrinaTardio Nov 28, 2024
ee2f891
have only one interface method
SabrinaTardio Nov 28, 2024
b3224fe
fix lint
SabrinaTardio Nov 28, 2024
b057431
address comments
SabrinaTardio Nov 28, 2024
e92cbb5
Merge branch 'sabrina/experiment-framework' into sabrina/experiment-p…
SabrinaTardio Nov 28, 2024
4756b60
update after merge
SabrinaTardio Nov 29, 2024
4f2d28b
Merge branch 'main' into sabrina/experiment-pixels
SabrinaTardio Nov 29, 2024
d202980
cleanup
SabrinaTardio Nov 29, 2024
bfb7a5e
fix lint errors
SabrinaTardio Nov 29, 2024
a98e9c4
add pixel unique parameter test
SabrinaTardio Nov 29, 2024
7747bdc
remove unwanted code
SabrinaTardio Nov 29, 2024
92bb24f
refactor
SabrinaTardio Nov 29, 2024
e7801f9
refactor and add more tests
SabrinaTardio Dec 2, 2024
fc1c31e
add tests
SabrinaTardio Dec 2, 2024
f03d0e3
Merge branch 'main' into sabrina/experiment-pixels
SabrinaTardio Dec 2, 2024
f24740a
fix linting
SabrinaTardio Dec 2, 2024
aef82dd
linting
SabrinaTardio Dec 2, 2024
9b65cc7
linting
SabrinaTardio Dec 2, 2024
4992efe
implement firing enrolment pixel
SabrinaTardio Dec 3, 2024
4412bd4
Merge branch 'main' into sabrina/experiment-pixels
SabrinaTardio Dec 3, 2024
b202b44
fix tests
SabrinaTardio Dec 3, 2024
c62fac4
generalise headers and prefix name
SabrinaTardio Dec 3, 2024
cc6c1b6
address comments
SabrinaTardio Dec 4, 2024
b567a22
add isDry for new unique params
SabrinaTardio Dec 4, 2024
2b0f434
fix linting
SabrinaTardio Dec 4, 2024
15eed7b
fix tests
SabrinaTardio Dec 4, 2024
e0e45ca
fix iOS pixel naming
SabrinaTardio Dec 4, 2024
cd6d89a
address comments and remove m_ from experiments
SabrinaTardio Dec 5, 2024
9568123
remove unwanted change
SabrinaTardio Dec 5, 2024
8db91da
fix mac pixel names
SabrinaTardio Dec 9, 2024
8c2a9c6
fix test
SabrinaTardio Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/PixelExperimentKit/ExperimentEventTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

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
Expand All @@ -38,18 +42,18 @@ public protocol ExperimentEventTracking {
/// - 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: String, threshold: Int, isInWindow: Bool) -> Bool
func incrementAndCheckThreshold(forKey key: ExprimentPixelNameAndParameters, threshold: NumberOfActions, isInWindow: Bool) -> ThresholdCheckResult
}

public struct ExperimentEventTracker: ExperimentEventTracking {
let store: ExperimentActionPixelStore
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: String, threshold: Int, isInWindow: Bool) -> Bool {
public func incrementAndCheckThreshold(forKey key: ExprimentPixelNameAndParameters, threshold: NumberOfActions, isInWindow: Bool) -> ThresholdCheckResult {
syncQueue.sync {
// Remove the key if is not in window
guard isInWindow else {
Expand Down
29 changes: 21 additions & 8 deletions Sources/PixelExperimentKit/PixelExperimentKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import PixelKit
import BrowserServicesKit
import Foundation

public typealias ConversionWindow = ClosedRange<Int>

struct ExperimentEvent: PixelKitEvent {
var name: String
var parameters: [String: String]?
Expand Down Expand Up @@ -81,15 +83,22 @@ extension PixelKit {
/// 1. Validates if the experiment is active.
/// 2. Ensures the user is within the specified conversion window.
/// 3. Tracks actions performed and sends the pixel once the target value is reached (if applicable).
public static func fireExperimentPixel(for subfeatureID: SubfeatureID, metric: String, conversionWindowDays: ClosedRange<Int>, value: String) {
public static func fireExperimentPixel(for subfeatureID: SubfeatureID,
metric: String,
conversionWindowDays: ConversionWindow,
value: String) {
// Check is active experiment for user
guard let featureFlagger = ExperimentConfig.featureFlagger else {
assertionFailure("PixelKit is not configured for experiments")
return
}
guard let experimentData = featureFlagger.getAllActiveExperiments()[subfeatureID] else { return }

fireExperimentPixelForActiveExperiment(subfeatureID, experimentData: experimentData, metric: metric, conversionWindowDays: conversionWindowDays, value: value)
fireExperimentPixelForActiveExperiment(subfeatureID,
experimentData: experimentData,
metric: metric,
conversionWindowDays: conversionWindowDays,
value: value)
}

/// Fires search-related experiment pixels for all active experiments.
Expand All @@ -99,7 +108,7 @@ extension PixelKit {
/// - The value and conversion windows define when and how many search actions
/// must occur before the pixel is fired.
public static func fireSearchExperimentPixels() {
let valueConversionDictionary: [Int: [ClosedRange<Int>]] = [
let valueConversionDictionary: [NumberOfActions: [ConversionWindow]] = [
1: [0...0, 1...1, 2...2, 3...3, 4...4, 5...5, 6...6, 7...7, 5...7],
4: [5...7, 8...15],
6: [5...7, 8...15],
Expand Down Expand Up @@ -128,7 +137,7 @@ extension PixelKit {
/// - The value and conversion windows define when and how many app usage actions
/// must occur before the pixel is fired.
public static func fireAppRetentionExperimentPixels() {
let valueConversionDictionary: [Int: [ClosedRange<Int>]] = [
let valueConversionDictionary: [NumberOfActions: [ConversionWindow]] = [
1: [1...1, 2...2, 3...3, 4...4, 5...5, 6...6, 7...7, 5...7],
4: [5...7, 8...15],
6: [5...7, 8...15],
Expand All @@ -154,7 +163,7 @@ extension PixelKit {
for experiment: SubfeatureID,
experimentData: ExperimentData,
metric: String,
valueConversionDictionary: [Int: [ClosedRange<Int>]]
valueConversionDictionary: [NumberOfActions: [ConversionWindow]]
) {
valueConversionDictionary.forEach { value, ranges in
ranges.forEach { range in
Expand All @@ -169,7 +178,11 @@ extension PixelKit {
}
}

private static func fireExperimentPixelForActiveExperiment(_ subfeatureID: SubfeatureID, experimentData: ExperimentData, metric: String, conversionWindowDays: ClosedRange<Int>, value: String) {
private static func fireExperimentPixelForActiveExperiment(_ subfeatureID: SubfeatureID,
experimentData: ExperimentData,
metric: String,
conversionWindowDays: ConversionWindow,
value: String) {
// Set parameters, event name, store key
let eventName = "\(Constants.metricsEventPrefix)_\(subfeatureID)_\(experimentData.cohortID)"
let conversionWindowValue = (conversionWindowDays.lowerBound != conversionWindowDays.upperBound) ?
Expand All @@ -188,7 +201,7 @@ extension PixelKit {
let isInWindow = isUserInConversionWindow(conversionWindowDays, enrollmentDate: experimentData.enrollmentDate)

// Check if value is a number
if let numberOfAction = Int(value), numberOfAction > 1 {
if let numberOfAction = NumberOfActions(value), numberOfAction > 1 {
// Increment or remove based on conversion window status
let shouldSendPixel = ExperimentConfig.eventTracker.incrementAndCheckThreshold(
forKey: eventStoreKey,
Expand All @@ -207,7 +220,7 @@ extension PixelKit {
}

private static func isUserInConversionWindow(
_ conversionWindowRange: ClosedRange<Int>,
_ conversionWindowRange: ConversionWindow,
enrollmentDate: Date
) -> Bool {
let calendar = Calendar.current
Expand Down
35 changes: 21 additions & 14 deletions Sources/PixelKit/PixelKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public final class PixelKit {
}

private var dryRun: Bool
private let source: String?
public let source: String?
SabrinaTardio marked this conversation as resolved.
Show resolved Hide resolved
private let pixelCalendar: Calendar

public init(dryRun: Bool,
Expand Down Expand Up @@ -396,8 +396,11 @@ public final class PixelKit {
fireRequest(pixelName, headers, parameters, allowedQueryReservedCharacters, callBackOnMainThread, onComplete)
}

// Needs to be updated when fully adopted by iOS
// Only set up for macOS and for Experiments
private func prefixedAndSuffixedName(for event: Event) -> String {
if event.name.hasPrefix("experiment") {
return addPlatformSuffix(to: event.name)
}
if event.name.hasPrefix("m_mac_") {
// Can be a debug event or not, if already prefixed the name remains unchanged
return event.name
Expand All @@ -408,22 +411,26 @@ public final class PixelKit {
// Special kind of pixel event that don't follow the standard naming conventions
return nonStandardEvent.name
} else {
if let source {
switch source {
case Source.iOS.rawValue:
return "m_\(event.name)_ios_phone"
case Source.iPadOS.rawValue:
return "m_\(event.name)_ios_tablet"
case Source.macDMG.rawValue, Source.macStore.rawValue:
return "m_mac_\(event.name)"
default:
return "m_mac_\(event.name)"
}
}
return "m_mac_\(event.name)"
}
}

private func addPlatformSuffix(to name: String) -> String {
if let source {
switch source {
case Source.iOS.rawValue:
return "\(name)_ios_phone"
case Source.iPadOS.rawValue:
return "\(name)_ios_tablet"
case Source.macStore.rawValue, Source.macDMG.rawValue:
return "\(name)_mac"
default:
return name
}
}
return name
}

public func fire(_ event: Event,
frequency: Frequency = .standard,
withHeaders headers: [String: String]? = nil,
Expand Down
Loading