Skip to content

Commit

Permalink
Add simple behavior monitoring to better address potential breakage i…
Browse files Browse the repository at this point in the history
…ssues (#2521)
  • Loading branch information
jaceklyp authored Feb 29, 2024
1 parent 19283cc commit a4cd443
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 2 deletions.
17 changes: 17 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,14 @@ extension Pixel {
case compilationFailed

case appRatingPromptFetchError

case userBehaviorReloadTwice
case userBehaviorReloadAndRestart
case userBehaviorReloadAndFireButton
case userBehaviorReloadAndOpenSettings
case userBehaviorReloadAndTogglePrivacyControls
case userBehaviorFireButtonAndRestart
case userBehaviorFireButtonAndTogglePrivacyControls
}

}
Expand Down Expand Up @@ -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"
}

}
Expand Down
4 changes: 4 additions & 0 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ public struct UserDefaultsWrapper<T> {
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
Expand Down
28 changes: 28 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2393,6 +2396,9 @@
CB2A7EF3285383B300885F67 /* AppLastCompiledRulesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLastCompiledRulesStore.swift; sourceTree = "<group>"; };
CB2C47822AF6D55800AEDCD9 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = "<group>"; };
CB4448752AF6D51D001F93F7 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
CB48D3302B90CE9F00631D8B /* UserBehaviorEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserBehaviorEvent.swift; sourceTree = "<group>"; };
CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitor.swift; sourceTree = "<group>"; };
CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBehaviorMonitorTests.swift; sourceTree = "<group>"; };
CB5038622AF6D563007FD69F /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
CB6ABD002AF6D52B004A8224 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
CB6CE65B2AF6D4EE00119848 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3721,6 +3727,7 @@
84E341941E2F7EFB00BDBA6F /* DuckDuckGo */ = {
isa = PBXGroup;
children = (
CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */,
EE3B98EA2A9634CC002F63A0 /* DuckDuckGoAlpha.entitlements */,
CB258D1129A4F1BB00DEBA24 /* Configuration */,
1E908BED29827C480008C8F3 /* Autoconsent */,
Expand Down Expand Up @@ -4486,6 +4493,23 @@
path = Configuration;
sourceTree = "<group>";
};
CB48D32F2B90CE8500631D8B /* UserBehaviorMonitor */ = {
isa = PBXGroup;
children = (
CB48D3302B90CE9F00631D8B /* UserBehaviorEvent.swift */,
CB48D3312B90CE9F00631D8B /* UserBehaviorMonitor.swift */,
);
name = UserBehaviorMonitor;
sourceTree = "<group>";
};
CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */ = {
isa = PBXGroup;
children = (
CB48D3352B90CECD00631D8B /* UserBehaviorMonitorTests.swift */,
);
name = UserBehaviorMonitor;
sourceTree = "<group>";
};
CBAA195627BFDD9800A4BD49 /* SmarterEncryption */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4912,6 +4936,7 @@
F12D98401F266B30003C2EE3 /* DuckDuckGo */ = {
isa = PBXGroup;
children = (
CB48D3342B90CEBD00631D8B /* UserBehaviorMonitor */,
F17669A21E411D63003D3222 /* Application */,
026F08B629B7DC130079B9DF /* AppTrackingProtection */,
981FED7222045FFA008488D7 /* AutoClear */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

clearDebugWaitlistState()

AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.reopenApp)

return true
}

Expand Down
7 changes: 7 additions & 0 deletions DuckDuckGo/AppDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import DDGSync
import Bookmarks

protocol DependencyProvider {

var appSettings: AppSettings { get }
var variantManager: VariantManager { get }
var internalUserDecider: InternalUserDecider { get }
Expand All @@ -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()
Expand All @@ -60,4 +64,7 @@ class AppDependencyProvider: DependencyProvider {
lazy var autofillNeverPromptWebsitesManager = AutofillNeverPromptWebsitesManager()

let configurationManager = ConfigurationManager()

let userBehaviorMonitor = UserBehaviorMonitor()

}
3 changes: 2 additions & 1 deletion DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class PrivacyDashboardViewController: UIViewController {
}

contentBlockingManager.scheduleCompilation()
AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.toggleProtections)
}

private func privacyDashboardCloseHandler() {
Expand Down
3 changes: 3 additions & 0 deletions DuckDuckGo/TabViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2090,6 +2091,8 @@ extension TabViewController: UIGestureRecognizerDelegate {
} else {
reload()
}

AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.refresh)
}

}
Expand Down
3 changes: 3 additions & 0 deletions DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ extension TabViewController {

private func onBrowsingSettingsAction() {
Pixel.fire(pixel: .browsingMenuSettings)
AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.openSettings)
delegate?.tabDidRequestSettings(tab: self)
}

Expand Down Expand Up @@ -441,5 +442,7 @@ extension TabViewController {
onAction: { [weak self] in
self?.togglePrivacyProtection(domain: domain)
})
AppDependencyProvider.shared.userBehaviorMonitor.handleAction(.toggleProtections)
}

}
32 changes: 32 additions & 0 deletions DuckDuckGo/UserBehaviorEvent.swift
Original file line number Diff line number Diff line change
@@ -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

}
115 changes: 115 additions & 0 deletions DuckDuckGo/UserBehaviorMonitor.swift
Original file line number Diff line number Diff line change
@@ -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<UserBehaviorEvent>
private var store: UserBehaviorStoring

init(eventMapping: EventMapping<UserBehaviorEvent> = 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<UserBehaviorEvent> { 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)
}

}
2 changes: 1 addition & 1 deletion DuckDuckGo/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGoTests/MockDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class MockDependencyProvider: DependencyProvider {
var autofillLoginSession: AutofillLoginSession
var autofillNeverPromptWebsitesManager: AutofillNeverPromptWebsitesManager
var configurationManager: ConfigurationManager
var userBehaviorMonitor: UserBehaviorMonitor

init() {
let defaultProvider = AppDependencyProvider()
Expand All @@ -51,5 +52,6 @@ class MockDependencyProvider: DependencyProvider {
autofillLoginSession = defaultProvider.autofillLoginSession
autofillNeverPromptWebsitesManager = defaultProvider.autofillNeverPromptWebsitesManager
configurationManager = defaultProvider.configurationManager
userBehaviorMonitor = defaultProvider.userBehaviorMonitor
}
}
Loading

0 comments on commit a4cd443

Please sign in to comment.