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

Add pixels for Privacy Stats on HTML NTP #3659

Merged
merged 12 commits into from
Dec 11, 2024
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
Loading