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

DBP: Implement stats pixels #2812

Merged
merged 4 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.initialScanTotalDuration,
.initialScanSiteLoadDuration,
.initialScanPostLoadingDuration,
.initialScanPreStartDuration:
.initialScanPreStartDuration,
.globalMetricsWeeklyStats,
.globalMetricsMonthlyStats,
.dataBrokerMetricsWeeklyStats,
.dataBrokerMetricsMonthlyStats:
PixelKit.fire(event)

case .homeViewShowNoPermissionError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ struct MirrorSite: Codable, Sendable {
removedAt = try? container.decode(Date.self, forKey: .removedAt)

}

func wasRemoved(since: Date = Date()) -> Bool {
guard let removedAt = self.removedAt else {
return false
}

return removedAt < since
}
}

public enum DataBrokerHierarchy: Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,13 @@ public struct HistoryEvent: Identifiable, Sendable {
return 0
}
}

func isMatchEvent() -> Bool {
switch type {
case .noMatchFound, .matchesFound:
return true
default:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager {
stageCalculator.fireScanSuccess(matchesFound: extractedProfiles.count)
let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .matchesFound(count: extractedProfiles.count))
try database.add(event)
let extractedProfilesForBroker = try database.fetchExtractedProfiles(for: brokerId)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ I changed this because we were fetching the extracted profiles from the database inside the loop. This improves how many times we go to the database.


for extractedProfile in extractedProfiles {

// We check if the profile exists in the database.
let extractedProfilesForBroker = try database.fetchExtractedProfiles(for: brokerId)
let doesProfileExistsInDatabase = extractedProfilesForBroker.contains { $0.identifier == extractedProfile.identifier }

// If the profile exists we do not create a new opt-out operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,6 @@ final class DataBrokerProtectionEngagementPixelsUserDefaults: DataBrokerProtecti
- MAU Pixel Last Sent 2024-03-19
*/
final class DataBrokerProtectionEngagementPixels {

enum ActiveUserFrequency: Int {
case daily = 1
case weekly = 7
case monthly = 28
}

private let calendar = Calendar.current
private let database: DataBrokerProtectionRepository
private let repository: DataBrokerProtectionEngagementPixelsRepository
private let handler: EventMapping<DataBrokerProtectionPixels>
Expand Down Expand Up @@ -139,36 +131,22 @@ final class DataBrokerProtectionEngagementPixels {
return true
}

return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily)
return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .daily)
}

private func shouldWeFireWeeklyPixel(date: Date) -> Bool {
guard let latestPixelFire = repository.getLatestWeeklyPixel() else {
return true
}

return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly)
return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .weekly)
}

private func shouldWeFireMonthlyPixel(date: Date) -> Bool {
guard let latestPixelFire = repository.getLatestMonthlyPixel() else {
return true
}

return shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly)
}

private func shouldWeFirePixel(startDate: Date, endDate: Date, daysDifference: ActiveUserFrequency) -> Bool {
if let differenceBetweenDates = differenceBetweenDates(startDate: startDate, endDate: endDate) {
return differenceBetweenDates >= daysDifference.rawValue
}

return false
}

private func differenceBetweenDates(startDate: Date, endDate: Date) -> Int? {
let components = calendar.dateComponents([.day], from: startDate, to: endDate)

return components.day
return DataBrokerProtectionPixelsUtilities.shouldWeFirePixel(startDate: latestPixelFire, endDate: date, daysDifference: .monthly)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ public enum DataBrokerProtectionPixels {
static let hasError = "has_error"
static let brokerURL = "broker_url"
static let sleepDuration = "sleep_duration"
static let numberOfRecordsFound = "num_found"
static let numberOfOptOutsInProgress = "num_inprogress"
static let numberOfSucessfulOptOuts = "num_optoutsuccess"
static let numberOfOptOutsFailure = "num_optoutfailure"
static let durationOfFirstOptOut = "duration_firstoptout"
static let numberOfNewRecordsFound = "num_new_found"
static let numberOfReappereances = "num_reappeared"
}

case error(error: DataBrokerProtectionError, dataBroker: String)
Expand Down Expand Up @@ -171,6 +178,12 @@ public enum DataBrokerProtectionPixels {
case entitlementCheckValid
case entitlementCheckInvalid
case entitlementCheckError
// Measure success/failure rate of Personal Information Removal Pixels
// https://app.asana.com/0/1204006570077678/1206889724879222/f
case globalMetricsWeeklyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int)
case globalMetricsMonthlyStats(profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int)
case dataBrokerMetricsWeeklyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int)
case dataBrokerMetricsMonthlyStats(dataBrokerURL: String, profilesFound: Int, optOutsInProgress: Int, successfulOptOuts: Int, failedOptOuts: Int, durationOfFirstOptOut: Int, numberOfNewRecordsFound: Int, numberOfReappereances: Int)
}

extension DataBrokerProtectionPixels: PixelKitEvent {
Expand Down Expand Up @@ -281,6 +294,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
case .entitlementCheckValid: return "m_mac_dbp_macos_entitlement_valid"
case .entitlementCheckInvalid: return "m_mac_dbp_macos_entitlement_invalid"
case .entitlementCheckError: return "m_mac_dbp_macos_entitlement_error"
case .globalMetricsWeeklyStats: return "m_mac_dbp_weekly_stats"
case .globalMetricsMonthlyStats: return "m_mac_dbp_monthly_stats"
case .dataBrokerMetricsWeeklyStats: return "m_mac_dbp_databroker_weekly_stats"
case .dataBrokerMetricsMonthlyStats: return "m_mac_dbp_databroker_monthly_stats"
}
}

Expand Down Expand Up @@ -419,6 +436,24 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
return [Consts.durationInMs: String(duration), Consts.hasError: hasError.description, Consts.brokerURL: brokerURL, Consts.sleepDuration: String(sleepDuration)]
case .initialScanPreStartDuration(let duration):
return [Consts.durationInMs: String(duration)]
case .globalMetricsWeeklyStats(let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound),
.globalMetricsMonthlyStats(let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound):
return [Consts.numberOfRecordsFound: String(profilesFound),
Consts.numberOfOptOutsInProgress: String(optOutsInProgress),
Consts.numberOfSucessfulOptOuts: String(successfulOptOuts),
Consts.numberOfOptOutsFailure: String(failedOptOuts),
Consts.durationOfFirstOptOut: String(durationOfFirstOptOut),
Consts.numberOfNewRecordsFound: String(numberOfNewRecordsFound)]
case .dataBrokerMetricsWeeklyStats(let dataBrokerURL, let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound, let numberOfReappereances),
.dataBrokerMetricsMonthlyStats(let dataBrokerURL, let profilesFound, let optOutsInProgress, let successfulOptOuts, let failedOptOuts, let durationOfFirstOptOut, let numberOfNewRecordsFound, let numberOfReappereances):
return [Consts.dataBrokerParamKey: dataBrokerURL,
Consts.numberOfRecordsFound: String(profilesFound),
Consts.numberOfOptOutsInProgress: String(optOutsInProgress),
Consts.numberOfSucessfulOptOuts: String(successfulOptOuts),
Consts.numberOfOptOutsFailure: String(failedOptOuts),
Consts.durationOfFirstOptOut: String(durationOfFirstOptOut),
Consts.numberOfNewRecordsFound: String(numberOfNewRecordsFound),
Consts.numberOfReappereances: String(numberOfReappereances)]
}
}
}
Expand Down Expand Up @@ -498,7 +533,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.initialScanTotalDuration,
.initialScanSiteLoadDuration,
.initialScanPostLoadingDuration,
.initialScanPreStartDuration:
.initialScanPreStartDuration,
.globalMetricsWeeklyStats,
.globalMetricsMonthlyStats,
.dataBrokerMetricsWeeklyStats,
.dataBrokerMetricsMonthlyStats:

PixelKit.fire(event)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// DataBrokerProtectionPixelsUtilities.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

enum Frequency: Int {
case daily = 1
case weekly = 7
case monthly = 28
}

final class DataBrokerProtectionPixelsUtilities {
private static let calendar = Calendar.current

static func shouldWeFirePixel(startDate: Date, endDate: Date, daysDifference: Frequency) -> Bool {
if let differenceBetweenDates = numberOfDaysFrom(startDate: startDate, endDate: endDate) {
return differenceBetweenDates >= daysDifference.rawValue
Copy link

@q71114 q71114 May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the definition of rolling windows (comparing to last sent pixel), but for these stats we're requested to send them using fixed windows from the beginning: every X days from the beginning - if one is missed then send the next one asap and then calculate the windows from the beginning again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@q71114 We have two things here: the date from when we calculated the stats and the date from when we fired the pixel.

This one is only to calculate when we should fire the pixel; we use the date from the beginning to calculate the stats.

For example, We fire the weekly pixel (calculating the stats since the beginning), save the date when it was fired, and when seven or more days pass, we fire it again, always calculating the stats from the beginning.

}

return false
}

static func numberOfDaysFrom(startDate: Date, endDate: Date) -> Int? {
let components = calendar.dateComponents([.day], from: startDate, to: endDate)

return components.day
}
}
Loading
Loading