Skip to content

Commit

Permalink
Add pixels for Privacy Stats on HTML NTP (#3659)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/69071770703008/1208936504720914/f

Description:
This change adds 2 new messages to track actions in the Privacy Stats widget
and implements usage and debug pixels for Privacy Stats.
  • Loading branch information
ayoy authored Dec 11, 2024
1 parent 7854b22 commit 2a6eb56
Show file tree
Hide file tree
Showing 18 changed files with 541 additions and 79 deletions.
45 changes: 37 additions & 8 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

#if DEBUG
if NSApplication.runType.requiresEnvironment {
privacyStats = PrivacyStats(databaseProvider: PrivacyStatsDatabase())
privacyStats = PrivacyStats(databaseProvider: PrivacyStatsDatabase(), errorEvents: PrivacyStatsErrorHandler())
} else {
privacyStats = MockPrivacyStats()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extension NewTabPageActionsManager {
let privacyStatsModel = NewTabPagePrivacyStatsModel(
privacyStats: privacyStats,
trackerDataProvider: PrivacyStatsTrackerDataProvider(contentBlocking: ContentBlocking.shared),
eventMapping: NewTabPagePrivacyStatsEventHandler(),
getLegacyIsViewExpandedSetting: UserDefaultsWrapper<Bool>(key: .homePageShowRecentlyVisited, defaultValue: false).wrappedValue
)

Expand All @@ -43,7 +44,7 @@ extension NewTabPageActionsManager {
self.init(scriptClients: [
NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences),
NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel),
NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())),
NewTabPageNextStepsCardsClient(model: NewTabPageNextStepsCardsProvider(continueSetUpModel: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener()))),
NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)),
NewTabPagePrivacyStatsClient(model: privacyStatsModel)
])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// ContinueSetUpModel+NewTabPage.swift
// NewTabPageNextStepsCardsProvider.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
Expand All @@ -22,43 +22,66 @@ import NewTabPage
import PixelKit
import UserScript

extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding {
final class NewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding {
let continueSetUpModel: HomePage.Models.ContinueSetUpModel
let appearancePreferences: AppearancePreferences

init(continueSetUpModel: HomePage.Models.ContinueSetUpModel, appearancePreferences: AppearancePreferences = .shared) {
self.continueSetUpModel = continueSetUpModel
self.appearancePreferences = appearancePreferences
}

var isViewExpanded: Bool {
get {
shouldShowAllFeatures
continueSetUpModel.shouldShowAllFeatures
}
set {
shouldShowAllFeatures = newValue
continueSetUpModel.shouldShowAllFeatures = newValue
}
}

var isViewExpandedPublisher: AnyPublisher<Bool, Never> {
shouldShowAllFeaturesPublisher.eraseToAnyPublisher()
continueSetUpModel.shouldShowAllFeaturesPublisher.eraseToAnyPublisher()
}

var cards: [NewTabPageNextStepsCardsClient.CardID] {
featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) }
guard !appearancePreferences.isContinueSetUpCardsViewOutdated else {
return []
}
return continueSetUpModel.featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) }
}

var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> {
$featuresMatrix.dropFirst().removeDuplicates()
.map { matrix in
matrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) }
let features = continueSetUpModel.$featuresMatrix.dropFirst().removeDuplicates()
let cardsDidBecomeOutdated = appearancePreferences.$isContinueSetUpCardsViewOutdated.removeDuplicates()

return Publishers.CombineLatest(features, cardsDidBecomeOutdated)
.map { features, isOutdated -> [NewTabPageNextStepsCardsClient.CardID] in
guard !isOutdated else {
return []
}
return features.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) }
}
.eraseToAnyPublisher()
}

@MainActor
func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) {
performAction(for: .init(card))
continueSetUpModel.performAction(for: .init(card))
}

@MainActor
func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) {
removeItem(for: .init(card))
continueSetUpModel.removeItem(for: .init(card))
}

@MainActor
func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) {
appearancePreferences.continueSetUpCardsViewDidAppear()
fireAddToDockPixelIfNeeded(cards)
}

private func fireAddToDockPixelIfNeeded(_ cards: [NewTabPageNextStepsCardsClient.CardID]) {
guard cards.contains(.addAppToDockMac) else {
return
}
Expand Down
39 changes: 39 additions & 0 deletions DuckDuckGo/NewTabPage/NewTabPagePrivacyStatsEventHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// NewTabPagePrivacyStatsEventHandler.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 Common
import PixelKit
import NewTabPage

final class NewTabPagePrivacyStatsEventHandler: EventMapping<NewTabPagePrivacyStatsEvent> {

init() {
super.init { event, _, _, _ in
switch event {
case .showLess:
PixelKit.fire(NewTabPagePixel.blockedTrackingAttemptsShowLess, frequency: .dailyAndCount)
case .showMore:
PixelKit.fire(NewTabPagePixel.blockedTrackingAttemptsShowMore, frequency: .dailyAndCount)
}
}
}

override init(mapping: @escaping EventMapping<NewTabPagePrivacyStatsEvent>.Mapping) {
fatalError("Use init()")
}
}
4 changes: 2 additions & 2 deletions DuckDuckGo/PrivacyStats/PrivacyStatsDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ public final class PrivacyStatsDatabase: PrivacyStatsDatabaseProviding {
db.loadStore { context, error in
guard context != nil else {
if let error = error {
PixelKit.fire(DebugEvent(GeneralPixel.privacyStatsCouldNotLoadDatabase, error: error))
PixelKit.fire(DebugEvent(NewTabPagePixel.privacyStatsCouldNotLoadDatabase, error: error), frequency: .dailyAndCount)
} else {
PixelKit.fire(DebugEvent(GeneralPixel.privacyStatsCouldNotLoadDatabase))
PixelKit.fire(DebugEvent(NewTabPagePixel.privacyStatsCouldNotLoadDatabase), frequency: .dailyAndCount)
}

Thread.sleep(forTimeInterval: 1)
Expand Down
34 changes: 34 additions & 0 deletions DuckDuckGo/PrivacyStats/PrivacyStatsErrorHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// PrivacyStatsErrorHandler.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 Common
import PixelKit
import PrivacyStats

final class PrivacyStatsErrorHandler: EventMapping<PrivacyStatsError> {

init() {
super.init { event, _, _, _ in
PixelKit.fire(DebugEvent(NewTabPagePixel.privacyStatsDatabaseError, error: event), frequency: .dailyAndCount)
}
}

override init(mapping: @escaping EventMapping<PrivacyStatsError>.Mapping) {
fatalError("Use init()")
}
}
6 changes: 0 additions & 6 deletions DuckDuckGo/Statistics/GeneralPixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -464,9 +464,6 @@ enum GeneralPixel: PixelKitEventV2 {
case siteNotWorkingShown
case siteNotWorkingWebsiteIsBroken

// Privacy Stats
case privacyStatsCouldNotLoadDatabase

var name: String {
switch self {

Expand Down Expand Up @@ -1144,9 +1141,6 @@ enum GeneralPixel: PixelKitEventV2 {
case .pageRefreshThreeTimesWithin20Seconds: return "m_mac_reload-three-times-within-20-seconds"
case .siteNotWorkingShown: return "m_mac_site-not-working_shown"
case .siteNotWorkingWebsiteIsBroken: return "m_mac_site-not-working_website-is-broken"

// Privacy Stats
case .privacyStatsCouldNotLoadDatabase: return "privacy_stats_could_not_load_database"
}
}

Expand Down
97 changes: 97 additions & 0 deletions DuckDuckGo/Statistics/NewTabPagePixel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// NewTabPagePixel.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
import PixelKit

/**
* This enum keeps pixels related to HTML New Tab Page.
*
* > Related links:
* [Privacy Triage](https://app.asana.com/0/69071770703008/1208146890364172/f)
* [Detailed Pixels description](https://app.asana.com/0/1201621708115095/1207983904350396/f)
*/
enum NewTabPagePixel: PixelKitEventV2 {

/**
* Event Trigger: "Show Less" button is clicked in Privacy Stats table on the New Tab Page, to collapse the table.
*
* > Note: This isn't the section collapse setting (like for Favorites or Next Steps), but the sub-setting
* to control whether the view should contain 5 most frequently blocked top companies or all top companies.
*
* Anomaly Investigation:
* - This pixel is fired from `NewTabPagePrivacyStatsModel` in response to a message sent by the user script.
* - In case of anomalies, check if the subscription between the user script and the model isn't causing the pixel
* to be fired more than once per interaction.
*/
case blockedTrackingAttemptsShowLess

/**
* Event Trigger: "Show More" button is clicked in Privacy Stats table on the New Tab Page, to expand the table.
*
* > Note: This isn't the section collapse setting (like for Favorites or Next Steps), but the sub-setting
* to control whether the view should contain 5 most frequently blocked top companies or all top companies.
*
* Anomaly Investigation:
* - This pixel is fired from `NewTabPagePrivacyStatsModel` in response to a message sent by the user script.
* - In case of anomalies, check if the subscription between the user script and the model isn't causing the pixel
* to be fired more than once per interaction.
*/
case blockedTrackingAttemptsShowMore

// MARK: - Debug

/**
* Event Trigger: Privacy Stats database fails to be initialized. Firing this pixel is followed by an app crash with a `fatalError`.
* This pixel can be fired when there's no space on disk, when database migration fails or when database was tampered with.
* This is a debug (health) pixel.
*
* Anomaly Investigation:
* - If this spikes in production it may mean we've released a new PriacyStats database model version
* and didn't handle migration correctly in which case we need a hotfix.
* - Otherwise it may happen occasionally for users with not space left on device.
*/
case privacyStatsCouldNotLoadDatabase

/**
* Event Trigger: Privacy Stats reports a database error when fetching, storing or clearing data,
* as outlined by `PrivacyStatsError`. This is a debug (health) pixel.
*
* Anomaly Investigation:
* - The errors here are all Core Data errors. The error code identifies the specific enum case of `PrivacyStatsError`.
* - Check `PrivacyStats` for places where the error is thrown.
*/
case privacyStatsDatabaseError

var name: String {
switch self {
case .blockedTrackingAttemptsShowLess: return "m_mac_new-tab-page_blocked-tracking-attempts_show-less"
case .blockedTrackingAttemptsShowMore: return "m_mac_new-tab-page_blocked-tracking-attempts_show-more"
case .privacyStatsCouldNotLoadDatabase: return "new-tab-page_privacy-stats_could-not-load-database"
case .privacyStatsDatabaseError: return "new-tab-page_privacy-stats_database_error"
}
}

var parameters: [String: String]? {
nil
}

var error: (any Error)? {
nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient {

willDisplayCardsPublisher
.sink { cards in
model.willDisplayCards(cards)
Task { @MainActor in
model.willDisplayCards(cards)
}
}
.store(in: &cancellables)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ public protocol NewTabPageNextStepsCardsProviding: AnyObject {
@MainActor
func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID)

@MainActor
func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID])
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient {
case onConfigUpdate = "stats_onConfigUpdate"
case onDataUpdate = "stats_onDataUpdate"
case setConfig = "stats_setConfig"
case showLess = "stats_showLess"
case showMore = "stats_showMore"
}

public init(model: NewTabPagePrivacyStatsModel) {
Expand All @@ -60,7 +62,9 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient {
userScript.registerMessageHandlers([
MessageName.getConfig.rawValue: { [weak self] in try await self?.getConfig(params: $0, original: $1) },
MessageName.getData.rawValue: { [weak self] in try await self?.getData(params: $0, original: $1) },
MessageName.setConfig.rawValue: { [weak self] in try await self?.setConfig(params: $0, original: $1) }
MessageName.setConfig.rawValue: { [weak self] in try await self?.setConfig(params: $0, original: $1) },
MessageName.showLess.rawValue: { [weak self] in try await self?.showLess(params: $0, original: $1) },
MessageName.showMore.rawValue: { [weak self] in try await self?.showMore(params: $0, original: $1) }
])
}

Expand Down Expand Up @@ -94,6 +98,18 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient {
private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? {
return await model.calculatePrivacyStats()
}

@MainActor
private func showLess(params: Any, original: WKScriptMessage) async throws -> Encodable? {
model.showLess()
return nil
}

@MainActor
private func showMore(params: Any, original: WKScriptMessage) async throws -> Encodable? {
model.showMore()
return nil
}
}

extension NewTabPagePrivacyStatsClient {
Expand Down
Loading

0 comments on commit 2a6eb56

Please sign in to comment.