From a4cd443cde1d062639b7923b3ec6dd112eff26ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 1 Mar 2024 00:59:42 +0100 Subject: [PATCH 1/2] Add simple behavior monitoring to better address potential breakage issues (#2521) --- Core/PixelEvent.swift | 17 ++ Core/UserDefaultsPropertyWrapper.swift | 4 + DuckDuckGo.xcodeproj/project.pbxproj | 28 +++ DuckDuckGo/AppDelegate.swift | 2 + DuckDuckGo/AppDependencyProvider.swift | 7 + DuckDuckGo/MainViewController.swift | 3 +- .../PrivacyDashboardViewController.swift | 1 + DuckDuckGo/TabViewController.swift | 3 + ...bViewControllerBrowsingMenuExtension.swift | 3 + DuckDuckGo/UserBehaviorEvent.swift | 32 ++++ DuckDuckGo/UserBehaviorMonitor.swift | 115 +++++++++++ DuckDuckGo/en.lproj/Localizable.strings | 2 +- DuckDuckGoTests/MockDependencyProvider.swift | 2 + .../UserBehaviorMonitorTests.swift | 181 ++++++++++++++++++ 14 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 DuckDuckGo/UserBehaviorEvent.swift create mode 100644 DuckDuckGo/UserBehaviorMonitor.swift create mode 100644 DuckDuckGoTests/UserBehaviorMonitorTests.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index b1766ba89c..291db3d335 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -526,6 +526,14 @@ extension Pixel { case compilationFailed case appRatingPromptFetchError + + case userBehaviorReloadTwice + case userBehaviorReloadAndRestart + case userBehaviorReloadAndFireButton + case userBehaviorReloadAndOpenSettings + case userBehaviorReloadAndTogglePrivacyControls + case userBehaviorFireButtonAndRestart + case userBehaviorFireButtonAndTogglePrivacyControls } } @@ -1024,6 +1032,15 @@ extension Pixel.Event { case .debugReturnUserUpdateATB: return "m_debug_return_user_update_atb" case .appRatingPromptFetchError: return "m_d_app_rating_prompt_fetch_error" + + // MARK: - User behavior + case .userBehaviorReloadTwice: return "m_reload-twice" + case .userBehaviorReloadAndRestart: return "m_reload-and-restart" + case .userBehaviorReloadAndFireButton: return "m_reload-and-fire-button" + case .userBehaviorReloadAndOpenSettings: return "m_reload-and-open-settings" + case .userBehaviorReloadAndTogglePrivacyControls: return "m_reload-and-toggle-privacy-controls" + case .userBehaviorFireButtonAndRestart: return "m_fire-button-and-restart" + case .userBehaviorFireButtonAndTogglePrivacyControls: return "m_fire-button-and-toggle-privacy-controls" } } diff --git a/Core/UserDefaultsPropertyWrapper.swift b/Core/UserDefaultsPropertyWrapper.swift index 7d4a96f316..9ab2131af7 100644 --- a/Core/UserDefaultsPropertyWrapper.swift +++ b/Core/UserDefaultsPropertyWrapper.swift @@ -123,6 +123,10 @@ public struct UserDefaultsWrapper { case privacyConfigCustomURL = "com.duckduckgo.ios.privacyConfigCustomURL" case subscriptionIsActive = "com.duckduckgo.ios.subscruption.isActive" + + case didRefreshTimestamp = "com.duckduckgo.ios.userBehavior.didRefreshTimestamp" + case didBurnTimestamp = "com.duckduckgo.ios.userBehavior.didBurnTimestamp" + } private let key: Key diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a9b885e529..1ccc5e2138 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -749,6 +749,9 @@ CB2A7EEF283D185100885F67 /* RulesCompilationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EEE283D185100885F67 /* RulesCompilationMonitor.swift */; }; CB2A7EF128410DF700885F67 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF028410DF700885F67 /* PixelEvent.swift */; }; CB2A7EF4285383B300885F67 /* AppLastCompiledRulesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */; }; + CB48D3322B90CE9F00631D8B /* UserBehaviorEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3302B90CE9F00631D8B /* UserBehaviorEvent.swift */; }; + CB48D3332B90CE9F00631D8B /* UserBehaviorMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */; }; + CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */; }; CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */; }; CB5516D1286500290079B175 /* ContentBlockingRulesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA904C24FD2DB000D41DDF /* ContentBlockingRulesTests.swift */; }; CB5516D2286500290079B175 /* AtbServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F21DBD21121147002631A6 /* AtbServerTests.swift */; }; @@ -2393,6 +2396,9 @@ CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLastCompiledRulesStore.swift; sourceTree = ""; }; CB2C47822AF6D55800AEDCD9 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; CB4448752AF6D51D001F93F7 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = ""; }; + CB48D3302B90CE9F00631D8B /* UserBehaviorEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserBehaviorEvent.swift; sourceTree = ""; }; + CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitor.swift; sourceTree = ""; }; + CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitorTests.swift; sourceTree = ""; }; CB5038622AF6D563007FD69F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; CB6ABD002AF6D52B004A8224 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; CB6CE65B2AF6D4EE00119848 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -3721,6 +3727,7 @@ 84E341941E2F7EFB00BDBA6F /* DuckDuckGo */ = { isa = PBXGroup; children = ( + CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */, EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */, CB258D1129A4F1BB00DEBA24 /* Configuration */, 1E908BED29827C480008C8F3 /* Autoconsent */, @@ -4486,6 +4493,23 @@ path = Configuration; sourceTree = ""; }; + CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */ = { + isa = PBXGroup; + children = ( + CB48D3302B90CE9F00631D8B /* UserBehaviorEvent.swift */, + CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */, + ); + name = UserBehaviorMonitor; + sourceTree = ""; + }; + CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */ = { + isa = PBXGroup; + children = ( + CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */, + ); + name = UserBehaviorMonitor; + sourceTree = ""; + }; CBAA195627BFDD9800A4BD49 /* SmarterEncryption */ = { isa = PBXGroup; children = ( @@ -4912,6 +4936,7 @@ F12D98401F266B30003C2EE3 /* DuckDuckGo */ = { isa = PBXGroup; children = ( + CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */, F17669A21E411D63003D3222 /* Application */, 026F08B629B7DC130079B9DF /* AppTrackingProtection */, 981FED7222045FFA008488D7 /* AutoClear */, @@ -6594,6 +6619,7 @@ F4147354283BF834004AA7A5 /* AutofillContentScopeFeatureToggles.swift in Sources */, 986DA94A24884B18004A7E39 /* WebViewTransition.swift in Sources */, 31B524572715BB23002225AB /* WebJSAlert.swift in Sources */, + CB48D3322B90CE9F00631D8B /* UserBehaviorEvent.swift in Sources */, 8536A1FD2ACF114B003AC5BA /* Theme+DesignSystem.swift in Sources */, F114C55B1E66EB020018F95F /* NibLoading.swift in Sources */, D6BFCB612B7525160051FF81 /* SubscriptionPIRViewModel.swift in Sources */, @@ -6775,6 +6801,7 @@ 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, 310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */, 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */, + CB48D3332B90CE9F00631D8B /* UserBehaviorMonitor.swift in Sources */, 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */, 1E4DCF4E27B6A69600961E25 /* DownloadsListHostingController.swift in Sources */, 850F93DB2B594AB800823EEA /* ZippedPassKitPreviewHelper.swift in Sources */, @@ -6988,6 +7015,7 @@ B6AD9E3A28D456820019CDE9 /* PrivacyConfigurationManagerMock.swift in Sources */, F189AED71F18F6DE001EBAE1 /* TabTests.swift in Sources */, F13B4BFB1F18E3D900814661 /* TabsModelPersistenceExtensionTests.swift in Sources */, + CB48D3372B90DF2000631D8B /* UserBehaviorMonitorTests.swift in Sources */, 8528AE7E212EF5FF00D0BD74 /* AppRatingPromptTests.swift in Sources */, 981FED692201FE69008488D7 /* AutoClearSettingsScreenTests.swift in Sources */, 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */, diff --git a/DuckDuckGo/AppDelegate.swift b/DuckDuckGo/AppDelegate.swift index 01bcadace1..82d6381202 100644 --- a/DuckDuckGo/AppDelegate.swift +++ b/DuckDuckGo/AppDelegate.swift @@ -329,6 +329,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { clearDebugWaitlistState() + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp) + return true } diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift index b23caebde0..55ae87a87b 100644 --- a/DuckDuckGo/AppDependencyProvider.swift +++ b/DuckDuckGo/AppDependencyProvider.swift @@ -24,6 +24,7 @@ import DDGSync import Bookmarks protocol DependencyProvider { + var appSettings: AppSettings { get } var variantManager: VariantManager { get } var internalUserDecider: InternalUserDecider { get } @@ -36,11 +37,14 @@ protocol DependencyProvider { var autofillLoginSession: AutofillLoginSession { get } var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager { get } var configurationManager: ConfigurationManager { get } + var userBehaviorMonitor: UserBehaviorMonitor { get } + } /// Provides dependencies for objects that are not directly instantiated /// through `init` call (e.g. ViewControllers created from Storyboards). class AppDependencyProvider: DependencyProvider { + static var shared: DependencyProvider = AppDependencyProvider() let appSettings: AppSettings = AppUserDefaults() @@ -60,4 +64,7 @@ class AppDependencyProvider: DependencyProvider { lazy var autofillNeverPromptWebsitesManager = AutofillNeverPromptWebsitesManager() let configurationManager = ConfigurationManager() + + let userBehaviorMonitor = UserBehaviorMonitor() + } diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 0860b07266..c2f67cd89d 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -2225,7 +2225,8 @@ extension MainViewController: AutoClearWorker { func forgetAllWithAnimation(transitionCompletion: (() -> Void)? = nil, showNextDaxDialog: Bool = false) { let spid = Instruments.shared.startTimedEvent(.clearingData) Pixel.fire(pixel: .forgetAllExecuted) - + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.burn) + tabManager.prepareAllTabsExceptCurrentForDataClearing() fireButtonAnimator.animate { diff --git a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift index 68521a29dd..98283d584a 100644 --- a/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/PrivacyDashboardViewController.swift @@ -113,6 +113,7 @@ class PrivacyDashboardViewController: UIViewController { } contentBlockingManager.scheduleCompilation() + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.toggleProtections) } private func privacyDashboardCloseHandler() { diff --git a/DuckDuckGo/TabViewController.swift b/DuckDuckGo/TabViewController.swift index b0c7df98ea..da4fbb14f2 100644 --- a/DuckDuckGo/TabViewController.swift +++ b/DuckDuckGo/TabViewController.swift @@ -421,6 +421,7 @@ class TabViewController: UIViewController { guard let self else { return } self.reload() Pixel.fire(pixel: .pullToRefresh) + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.refresh) }, for: .valueChanged) webView.scrollView.refreshControl?.backgroundColor = .systemBackground @@ -2090,6 +2091,8 @@ extension TabViewController: UIGestureRecognizerDelegate { } else { reload() } + + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.refresh) } } diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index b946236540..2d4f1397c3 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -403,6 +403,7 @@ extension TabViewController { private func onBrowsingSettingsAction() { Pixel.fire(pixel: .browsingMenuSettings) + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.openSettings) delegate?.tabDidRequestSettings(tab: self) } @@ -441,5 +442,7 @@ extension TabViewController { onAction: { [weak self] in self?.togglePrivacyProtection(domain: domain) }) + AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.toggleProtections) } + } diff --git a/DuckDuckGo/UserBehaviorEvent.swift b/DuckDuckGo/UserBehaviorEvent.swift new file mode 100644 index 0000000000..1de6af8a92 --- /dev/null +++ b/DuckDuckGo/UserBehaviorEvent.swift @@ -0,0 +1,32 @@ +// +// UserBehaviorEvent.swift +// DuckDuckGo +// +// 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 + +public enum UserBehaviorEvent { + + case reloadTwice + case reloadAndRestart + case reloadAndFireButton + case reloadAndOpenSettings + case reloadAndTogglePrivacyControls + case fireButtonAndRestart + case fireButtonAndTogglePrivacyControls + +} diff --git a/DuckDuckGo/UserBehaviorMonitor.swift b/DuckDuckGo/UserBehaviorMonitor.swift new file mode 100644 index 0000000000..076c665c3c --- /dev/null +++ b/DuckDuckGo/UserBehaviorMonitor.swift @@ -0,0 +1,115 @@ +// +// UserBehaviorMonitor.swift +// DuckDuckGo +// +// 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 Common +import Core + +protocol UserBehaviorStoring { + + var didRefreshTimestamp: Date? { get set } + var didBurnTimestamp: Date? { get set } + +} + +final class UserBehaviorStore: UserBehaviorStoring { + + @UserDefaultsWrapper(key: .didRefreshTimestamp, defaultValue: .distantPast) + var didRefreshTimestamp: Date? + + @UserDefaultsWrapper(key: .didBurnTimestamp, defaultValue: .distantPast) + var didBurnTimestamp: Date? + +} + +final class UserBehaviorMonitor { + + enum Action: Equatable { + + case refresh + case burn + case reopenApp + case openSettings + case toggleProtections + + } + + private let eventMapping: EventMapping + private var store: UserBehaviorStoring + + init(eventMapping: EventMapping = AppUserBehaviorMonitor.eventMapping, + store: UserBehaviorStoring = UserBehaviorStore()) { + self.eventMapping = eventMapping + self.store = store + } + + var didRefreshTimestamp: Date? { + get { store.didRefreshTimestamp } + set { store.didRefreshTimestamp = newValue } + } + + var didBurnTimestamp: Date? { + get { store.didBurnTimestamp } + set { store.didBurnTimestamp = newValue } + } + + func handleAction(_ action: Action, date: Date = Date()) { + switch action { + case .refresh: + fireEventIfActionOccurredRecently(since: didRefreshTimestamp, eventToFire: .reloadTwice, within: 10.0) + didRefreshTimestamp = date + case .burn: + fireEventIfActionOccurredRecently(since: didRefreshTimestamp, eventToFire: .reloadAndFireButton) + didBurnTimestamp = date + case .reopenApp: + fireEventIfActionOccurredRecently(since: didRefreshTimestamp, eventToFire: .reloadAndRestart) + fireEventIfActionOccurredRecently(since: didBurnTimestamp, eventToFire: .fireButtonAndRestart) + case .openSettings: + fireEventIfActionOccurredRecently(since: didRefreshTimestamp, eventToFire: .reloadAndOpenSettings) + case .toggleProtections: + fireEventIfActionOccurredRecently(since: didRefreshTimestamp, eventToFire: .reloadAndTogglePrivacyControls) + fireEventIfActionOccurredRecently(since: didBurnTimestamp, eventToFire: .fireButtonAndTogglePrivacyControls) + } + + func fireEventIfActionOccurredRecently(since timestamp: Date?, eventToFire: UserBehaviorEvent, within interval: Double = 30.0) { + if let timestamp = timestamp, date.timeIntervalSince(timestamp) < interval { + eventMapping.fire(eventToFire) + } + } + } + +} + +final class AppUserBehaviorMonitor { + + static let eventMapping = EventMapping { event, _, _, _ in + let domainEvent: Pixel.Event + switch event { + case .reloadTwice: domainEvent = .userBehaviorReloadTwice + case .reloadAndRestart: domainEvent = .userBehaviorReloadAndRestart + case .reloadAndFireButton: domainEvent = .userBehaviorReloadAndFireButton + case .reloadAndOpenSettings: domainEvent = .userBehaviorReloadAndOpenSettings + case .reloadAndTogglePrivacyControls: domainEvent = .userBehaviorReloadAndTogglePrivacyControls + case .fireButtonAndRestart: domainEvent = .userBehaviorFireButtonAndRestart + case .fireButtonAndTogglePrivacyControls: domainEvent = .userBehaviorFireButtonAndTogglePrivacyControls + } + Pixel.fire(pixel: domainEvent) + } + +} diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 2445871097..6217982ed6 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2005,7 +2005,7 @@ But if you *do* want a peek under the hood, you can find more information about "subscription.manage.devices" = "Manage Devices"; /* Description for Email Management options */ -"subscription.manage.email.description" = "You can use this email to activate your subscription from browser settings in the DuckDuckGo app on your other devices."; +"subscription.manage.email.description" = "You can use this email to activate your subscription on your other devices."; /* Manage Plan header */ "subscription.manage.plan" = "Manage Plan"; diff --git a/DuckDuckGoTests/MockDependencyProvider.swift b/DuckDuckGoTests/MockDependencyProvider.swift index 7676843d3c..0b53f456a4 100644 --- a/DuckDuckGoTests/MockDependencyProvider.swift +++ b/DuckDuckGoTests/MockDependencyProvider.swift @@ -36,6 +36,7 @@ class MockDependencyProvider: DependencyProvider { var autofillLoginSession: AutofillLoginSession var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager var configurationManager: ConfigurationManager + var userBehaviorMonitor: UserBehaviorMonitor init() { let defaultProvider = AppDependencyProvider() @@ -51,5 +52,6 @@ class MockDependencyProvider: DependencyProvider { autofillLoginSession = defaultProvider.autofillLoginSession autofillNeverPromptWebsitesManager = defaultProvider.autofillNeverPromptWebsitesManager configurationManager = defaultProvider.configurationManager + userBehaviorMonitor = defaultProvider.userBehaviorMonitor } } diff --git a/DuckDuckGoTests/UserBehaviorMonitorTests.swift b/DuckDuckGoTests/UserBehaviorMonitorTests.swift new file mode 100644 index 0000000000..d95cf04c17 --- /dev/null +++ b/DuckDuckGoTests/UserBehaviorMonitorTests.swift @@ -0,0 +1,181 @@ +// +// UserBehaviorMonitorTests.swift +// DuckDuckGo +// +// 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 XCTest +import Common +@testable import DuckDuckGo + +final class MockUserBehaviorEventsMapping: EventMapping { + + init(captureEvent: @escaping (UserBehaviorEvent) -> Void) { + super.init { event, _, _, _ in + captureEvent(event) + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} + +final class MockUserBehaviorStore: UserBehaviorStoring { + + var didRefreshTimestamp: Date? + var didBurnTimestamp: Date? + +} + +final class UserBehaviorMonitorTests: XCTestCase { + + var eventMapping: MockUserBehaviorEventsMapping! + var monitor: UserBehaviorMonitor! + var events: [UserBehaviorEvent] = [] + + override func setUp() { + super.setUp() + events.removeAll() + eventMapping = MockUserBehaviorEventsMapping(captureEvent: { event in + self.events.append(event) + }) + monitor = UserBehaviorMonitor(eventMapping: eventMapping, + store: MockUserBehaviorStore()) + } + + // - MARK: Behavior testing + // Expecting events + + func testWhenUserRefreshesTwiceItSendsReloadTwiceEvent() { + monitor.handleAction(.refresh) + monitor.handleAction(.refresh) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadTwice) + } + + func testWhenUserRefreshesAndThenReopensAppItSendsReloadAndRestartEvent() { + monitor.handleAction(.refresh) + monitor.handleAction(.reopenApp) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadAndRestart) + } + + func testWhenUserRefreshesAndThenUsesFireButtonItSendsReloadAndFireButtonEvent() { + monitor.handleAction(.refresh) + monitor.handleAction(.burn) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadAndFireButton) + } + + func testWhenUserRefreshesAndThenOpensSettingsItSendsReloadAndOpenSettingsEvent() { + monitor.handleAction(.refresh) + monitor.handleAction(.openSettings) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadAndOpenSettings) + } + + func testWhenUserRefreshesAndThenTogglesProtectionsItSendsReloadAndTogglePrivacyControlsEvent() { + monitor.handleAction(.refresh) + monitor.handleAction(.toggleProtections) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadAndTogglePrivacyControls) + } + + func testWhenUserUsesFireButtonAndThenReopensAppItSendsFireButtonAndRestartEvent() { + monitor.handleAction(.burn) + monitor.handleAction(.reopenApp) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .fireButtonAndRestart) + } + + func testWhenUserUsesFireButtonAndThenTogglesProtectionsItSendsFireButtonAndTogglePrivacyControlsEvent() { + monitor.handleAction(.burn) + monitor.handleAction(.toggleProtections) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .fireButtonAndTogglePrivacyControls) + } + + func testWhenUserUsesFireButtonThenOpensSettingsThenReopensAppItSendsFireButtonAndRestartEvent() { + monitor.handleAction(.burn) + monitor.handleAction(.openSettings) + monitor.handleAction(.reopenApp) + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .fireButtonAndRestart) + } + + func testWhenUserUsesFireButtonThenRefreshesThenReopensAppItSendsTwoEvents() { + monitor.handleAction(.burn) + monitor.handleAction(.refresh) + monitor.handleAction(.reopenApp) + XCTAssertEqual(events.count, 2) + XCTAssertEqual(events[0], .reloadAndRestart) + XCTAssertEqual(events[1], .fireButtonAndRestart) + } + + func testWhenUserRefreshesThenReopensAppThenUsesFireButtonThenItSendsThreeEvents() { + monitor.handleAction(.refresh) + monitor.handleAction(.burn) + monitor.handleAction(.reopenApp) + XCTAssertEqual(events.count, 3) + XCTAssertEqual(events[0], .reloadAndFireButton) + XCTAssertEqual(events[1], .reloadAndRestart) + XCTAssertEqual(events[2], .fireButtonAndRestart) + } + + // Not expecting any events + + func testWhenUserUsesFireButtonAndThenRefreshesItShouldNotSendAnyEvent() { + monitor.handleAction(.burn) + monitor.handleAction(.refresh) + XCTAssertTrue(events.isEmpty) + } + + // Timing + + func testFireReloadTwiceEventOnlyIfItHappenedWithin10seconds() { + let date = Date() + monitor.handleAction(.refresh, date: date) + monitor.handleAction(.refresh, date: date + 10) // 10 seconds after the first event + XCTAssertTrue(events.isEmpty) + monitor.handleAction(.refresh, date: date + 15) // 5 seconds after the second event + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadTwice) + } + + func testFireReloadAndRestartEventOnlyIfItHappenedWithin30seconds() { + let date = Date() + monitor.handleAction(.refresh, date: date) + monitor.handleAction(.reopenApp, date: date + 30) // 30 seconds after the first event + XCTAssertTrue(events.isEmpty) + monitor.handleAction(.refresh, date: date + 30) + monitor.handleAction(.reopenApp, date: date + 50) // 20 seconds after the second event + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .reloadAndRestart) + } + + func testFireButtonAndRestartEventOnlyIfItHappenedWithin30seconds() { + let date = Date() + monitor.handleAction(.burn, date: date) + monitor.handleAction(.reopenApp, date: date + 30) // 30 seconds after the first event + XCTAssertTrue(events.isEmpty) + monitor.handleAction(.burn, date: date + 30) + monitor.handleAction(.reopenApp, date: date + 50) // 20 seconds after the second event + XCTAssertEqual(events.count, 1) + XCTAssertEqual(events.first!, .fireButtonAndRestart) + } + +} From e2e667e411d4e27269cb0d7a1d3e7911be52bafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Fri, 1 Mar 2024 01:05:47 +0100 Subject: [PATCH 2/2] Release 7.110.0-3 (#2523) --- DuckDuckGo.xcodeproj/project.pbxproj | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1ccc5e2138..c2ed546042 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -8189,7 +8189,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8226,7 +8226,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8318,7 +8318,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8346,7 +8346,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8496,7 +8496,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8522,7 +8522,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8587,7 +8587,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8622,7 +8622,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8656,7 +8656,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8687,7 +8687,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8974,7 +8974,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9005,7 +9005,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9034,7 +9034,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9068,7 +9068,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9099,7 +9099,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9132,11 +9132,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9374,7 +9374,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9401,7 +9401,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9434,7 +9434,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9472,7 +9472,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9508,7 +9508,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9543,11 +9543,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9721,11 +9721,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9754,10 +9754,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";