Skip to content

Commit

Permalink
Add stage granularity to Pixels
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Mar 14, 2024
1 parent ddf8b04 commit f99c05c
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 8 deletions.
1 change: 1 addition & 0 deletions DuckDuckGo/DBP/DBPHomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.optOutValidate,
.optOutFinish,
.optOutSubmitSuccess,
.optOutFillForm,
.optOutSuccess,
.optOutFailure,
.backgroundAgentStarted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ final class FakeStageDurationCalculator: StageDurationCalculator {
func fireOptOutEmailConfirm() {
}

func fireOptOutFillForm() {
}

func fireOptOutValidate() {
}

Expand All @@ -239,6 +242,9 @@ final class FakeStageDurationCalculator: StageDurationCalculator {

func setStage(_ stage: Stage) {
}

func setLastActionId(_ actionID: String) {
}
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,20 @@ extension DataBrokerOperation {

// MARK: - Shared functions

// swiftlint:disable:next cyclomatic_complexity
func runNextAction(_ action: Action) async {
switch action {
case is GetCaptchaInfoAction:
stageCalculator?.setStage(.captchaParse)
case is ClickAction:
stageCalculator?.setStage(.fillForm)
case is FillFormAction:
stageCalculator?.setStage(.fillForm)
case is ExpectationAction:
stageCalculator?.setStage(.submit)
default: ()
}

if let emailConfirmationAction = action as? EmailConfirmationAction {
do {
stageCalculator?.fireOptOutSubmit()
Expand Down Expand Up @@ -109,10 +122,6 @@ extension DataBrokerOperation {
}
}

if action as? GetCaptchaInfoAction != nil {
stageCalculator?.setStage(.captchaParse)
}

await webViewHandler?.execute(action: action, data: .userData(query.profileQuery, self.extractedProfile))
}

Expand Down Expand Up @@ -176,8 +185,12 @@ extension DataBrokerOperation {
func success(actionId: String, actionType: ActionType) async {
switch actionType {
case .click:
stageCalculator?.fireOptOutFillForm()
try? await webViewHandler?.waitForWebViewLoad(timeoutInSeconds: 30)
await executeNextStep()
case .fillForm:
stageCalculator?.fireOptOutFillForm()
await executeNextStep()
default: await executeNextStep()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ final class OptOutOperation: DataBrokerOperation {
try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000)

if let action = actionsHandler?.nextAction(), self.shouldRunNextStep() {
stageCalculator?.setLastActionId(action.id)
await runNextAction(action)
} else {
await webViewHandler?.finish() // If we executed all steps we release the web view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public enum DataBrokerProtectionPixels {
static let errorDetailsKey = "error_details"
static let pattern = "pattern"
static let isParent = "is_parent"
static let actionIDKey = "action_id"
}

case error(error: DataBrokerProtectionError, dataBroker: String)
Expand All @@ -69,11 +70,12 @@ public enum DataBrokerProtectionPixels {
case optOutEmailConfirm(dataBroker: String, attemptId: UUID, duration: Double)
case optOutValidate(dataBroker: String, attemptId: UUID, duration: Double)
case optOutFinish(dataBroker: String, attemptId: UUID, duration: Double)
case optOutFillForm(dataBroker: String, attemptId: UUID, duration: Double)

// Process Pixels
case optOutSubmitSuccess(dataBroker: String, attemptId: UUID, duration: Double, tries: Int, emailPattern: String?)
case optOutSuccess(dataBroker: String, attemptId: UUID, duration: Double, brokerType: DataBrokerHierarchy)
case optOutFailure(dataBroker: String, attemptId: UUID, duration: Double, stage: String, tries: Int, emailPattern: String?)
case optOutFailure(dataBroker: String, attemptId: UUID, duration: Double, stage: String, tries: Int, emailPattern: String?, actionID: String?)

// Backgrond Agent events
case backgroundAgentStarted
Expand Down Expand Up @@ -133,6 +135,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
case .optOutEmailConfirm: return "m_mac_dbp_macos_optout_stage_email-confirm"
case .optOutValidate: return "m_mac_dbp_macos_optout_stage_validate"
case .optOutFinish: return "m_mac_dbp_macos_optout_stage_finish"
case .optOutFillForm: return "m_mac_dbp_macos_optout_stage_fill-form"

// Process Pixels
case .optOutSubmitSuccess: return "m_mac_dbp_macos_optout_process_submit-success"
Expand Down Expand Up @@ -227,6 +230,8 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
return [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration)]
case .optOutFinish(let dataBroker, let attemptId, let duration):
return [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration)]
case .optOutFillForm(let dataBroker, let attemptId, let duration):
return [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration)]
case .optOutSubmitSuccess(let dataBroker, let attemptId, let duration, let tries, let pattern):
var params = [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries)]
if let pattern = pattern {
Expand All @@ -235,11 +240,16 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
return params
case .optOutSuccess(let dataBroker, let attemptId, let duration, let type):
return [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration), Consts.isParent: String(type.rawValue)]
case .optOutFailure(let dataBroker, let attemptId, let duration, let stage, let tries, let pattern):
case .optOutFailure(let dataBroker, let attemptId, let duration, let stage, let tries, let pattern, let actionID):
var params = [Consts.dataBrokerParamKey: dataBroker, Consts.attemptIdParamKey: attemptId.uuidString, Consts.durationParamKey: String(duration), Consts.stageKey: stage, Consts.triesKey: String(tries)]
if let pattern = pattern {
params[Consts.pattern] = pattern
}

if let actionID = actionID {
params[Consts.actionIDKey] = actionID
}

return params
case .backgroundAgentStarted,
.backgroundAgentRunOperationsAndStartSchedulerIfPossible,
Expand Down Expand Up @@ -303,6 +313,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.optOutValidate,
.optOutFinish,
.optOutSubmitSuccess,
.optOutFillForm,
.optOutSuccess,
.optOutFailure,
.backgroundAgentStarted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum Stage: String {
case emailConfirm = "email-confirm"
case validate
case other
case fillForm = "fill-form"
}

protocol StageDurationCalculator {
Expand All @@ -43,6 +44,7 @@ protocol StageDurationCalculator {
func fireOptOutCaptchaSend()
func fireOptOutCaptchaSolve()
func fireOptOutSubmit()
func fireOptOutFillForm()
func fireOptOutEmailReceive()
func fireOptOutEmailConfirm()
func fireOptOutValidate()
Expand All @@ -53,6 +55,7 @@ protocol StageDurationCalculator {
func fireScanError(error: Error)
func setStage(_ stage: Stage)
func setEmailPattern(_ emailPattern: String?)
func setLastActionId(_ actionID: String)
}

final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator {
Expand All @@ -61,6 +64,7 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator
let dataBroker: String
let startTime: Date
var lastStateTime: Date
private (set) var actionID: String?
private (set) var stage: Stage = .other
private (set) var emailPattern: String?

Expand Down Expand Up @@ -137,13 +141,18 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator
emailPattern: emailPattern))
}

func fireOptOutFillForm() {
handler.fire(.optOutFillForm(dataBroker: dataBroker, attemptId: attemptId, duration: durationSinceLastStage()))
}

func fireOptOutFailure(tries: Int) {
handler.fire(.optOutFailure(dataBroker: dataBroker,
attemptId: attemptId,
duration: durationSinceStartTime(),
stage: stage.rawValue,
tries: tries,
emailPattern: emailPattern))
emailPattern: emailPattern,
actionID: actionID))
}

func fireScanSuccess(matchesFound: Int) {
Expand Down Expand Up @@ -190,10 +199,15 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator
// identifying the stage so we can know which one was the one that failed.

func setStage(_ stage: Stage) {
lastStateTime = Date() // When we set a new stage we need to reset the lastStateTime so we count from there
self.stage = stage
}

func setEmailPattern(_ emailPattern: String?) {
self.emailPattern = emailPattern
}

func setLastActionId(_ actionID: String) {
self.actionID = actionID
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,80 @@ final class DataBrokerOperationActionTests: XCTestCase {

XCTAssertEqual(webViewHandler.wasLoadCalledWithURL?.absoluteString, "https://www.duckduckgo.com")
}

func testWhenGetCaptchaActionRuns_thenStageIsSetToCaptchaParse() async {
let mockStageCalculator = MockStageDurationCalculator()
let captchaAction = GetCaptchaInfoAction(id: "1", actionType: .getCaptchaInfo, selector: "captcha", dataSource: nil)
let sut = OptOutOperation(
privacyConfig: PrivacyConfigurationManagingMock(),
prefs: ContentScopeProperties.mock,
query: BrokerProfileQueryData.mock(),
emailService: emailService,
operationAwaitTime: 0,
shouldRunNextStep: { true }
)

sut.stageCalculator = mockStageCalculator

await sut.runNextAction(captchaAction)

XCTAssertEqual(mockStageCalculator.stage, .captchaParse)
}

func testWhenClickActionRuns_thenStageIsSetToSubmit() async {
let mockStageCalculator = MockStageDurationCalculator()
let clickAction = ClickAction(id: "1", actionType: .click, elements: [PageElement](), dataSource: nil)
let sut = OptOutOperation(
privacyConfig: PrivacyConfigurationManagingMock(),
prefs: ContentScopeProperties.mock,
query: BrokerProfileQueryData.mock(),
emailService: emailService,
operationAwaitTime: 0,
shouldRunNextStep: { true }
)

sut.stageCalculator = mockStageCalculator

await sut.runNextAction(clickAction)

XCTAssertEqual(mockStageCalculator.stage, .fillForm)
}

func testWhenExpectationActionRuns_thenStageIsSetToSubmit() async {
let mockStageCalculator = MockStageDurationCalculator()
let expectationAction = ExpectationAction(id: "1", actionType: .expectation, expectations: [Item](), dataSource: nil)
let sut = OptOutOperation(
privacyConfig: PrivacyConfigurationManagingMock(),
prefs: ContentScopeProperties.mock,
query: BrokerProfileQueryData.mock(),
emailService: emailService,
operationAwaitTime: 0,
shouldRunNextStep: { true }
)

sut.stageCalculator = mockStageCalculator

await sut.runNextAction(expectationAction)

XCTAssertEqual(mockStageCalculator.stage, .submit)
}

func testWhenFillFormActionRuns_thenStageIsSetToFillForm() async {
let mockStageCalculator = MockStageDurationCalculator()
let fillFormAction = FillFormAction(id: "1", actionType: .fillForm, selector: "", elements: [PageElement](), dataSource: nil)
let sut = OptOutOperation(
privacyConfig: PrivacyConfigurationManagingMock(),
prefs: ContentScopeProperties.mock,
query: BrokerProfileQueryData.mock(),
emailService: emailService,
operationAwaitTime: 0,
shouldRunNextStep: { true }
)

sut.stageCalculator = mockStageCalculator

await sut.runNextAction(fillFormAction)

XCTAssertEqual(mockStageCalculator.stage, .fillForm)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ final class DataBrokerProfileQueryOperationManagerTests: XCTestCase {
} catch {
if let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.last {
switch lastPixelFired {
case .optOutFailure(_, _, _, _, let tries, _):
case .optOutFailure(_, _, _, _, let tries, _, _):
XCTAssertEqual(tries, 3)
default: XCTFail("We should be firing the opt-out submit-success pixel last")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,74 @@ final class MockAppVersion: AppVersionNumberProvider {
self.versionNumber = versionNumber
}
}

final class MockStageDurationCalculator: StageDurationCalculator {
var stage: Stage?

func durationSinceLastStage() -> Double {
return 0.0
}

func durationSinceStartTime() -> Double {
return 0.0
}

func fireOptOutStart() {
}

func fireOptOutEmailGenerate() {
}

func fireOptOutCaptchaParse() {
}

func fireOptOutCaptchaSend() {
}

func fireOptOutCaptchaSolve() {
}

func fireOptOutSubmit() {
}

func fireOptOutEmailReceive() {
}

func fireOptOutEmailConfirm() {
}

func fireOptOutValidate() {
}

func fireOptOutSubmitSuccess(tries: Int) {
}

func fireOptOutFillForm() {
}

func fireOptOutFailure(tries: Int) {
}

func fireScanSuccess(matchesFound: Int) {
}

func fireScanFailed() {
}

func fireScanError(error: any Error) {
}

func setStage(_ stage: DataBrokerProtection.Stage) {
self.stage = stage
}

func setEmailPattern(_ emailPattern: String?) {
}

func setLastActionId(_ actionID: String) {
}

func clear() {
self.stage = nil
}
}

0 comments on commit f99c05c

Please sign in to comment.