diff --git a/iOS/Core/Pixel.swift b/iOS/Core/Pixel.swift index 317c3a47e64..6a27b2de81a 100644 --- a/iOS/Core/Pixel.swift +++ b/iOS/Core/Pixel.swift @@ -86,6 +86,7 @@ public struct PixelParameters { public static let count = "count" public static let source = "source" + public static let browsingMode = "browsing_mode" public static let authVersion = "authVersion" public static let lastUsed = "last_used" diff --git a/iOS/Core/PixelEvent.swift b/iOS/Core/PixelEvent.swift index 7308721e53c..7a0cddf566e 100644 --- a/iOS/Core/PixelEvent.swift +++ b/iOS/Core/PixelEvent.swift @@ -1741,6 +1741,7 @@ extension Pixel { case webExtensionAdBlockingInstalled case webExtensionAdBlockingUpgraded case webExtensionAdBlockingInstallError + case webExtensionAdBlockingSettingsOpen case webExtensionAdBlockingEnabled case webExtensionAdBlockingDisabled @@ -1752,6 +1753,25 @@ extension Pixel { case webExtensionScriptletInstallError case webExtensionDailyAdBlockingState + + // MARK: - Fire Mode + case fireModeNTPPromotionShown + case fireModeNTPPromotionDismissed + case fireModeNTPPromotionEngaged + case fireModeMenuPromotionShown + case fireModeMenuPromotionEngaged + case browsingModeSwitched + case tabSwitcherModeToggled + case fireModeBurnExecuted + case normalModeBurnExecuted + case fireModeDataCleared + case normalModeDataCleared + case fireModeLastTabClosedBurn + case fireModeEmptyStateNewTab + case linkLongPressMenuShown + case linkLongPressNewTab + case linkLongPressBackgroundTab + case linkLongPressFireTab } } @@ -3404,9 +3424,6 @@ extension Pixel.Event { case .webExtensionAdBlockingInstalled: return "m_web_extension_ad_blocking_installed" case .webExtensionAdBlockingUpgraded: return "m_web_extension_ad_blocking_upgraded" case .webExtensionAdBlockingInstallError: return "m_web_extension_ad_blocking_install_error" - case .webExtensionAdBlockingSettingsOpen: return "m_web_extension_ad_blocking_settings_open" - case .webExtensionAdBlockingEnabled: return "m_web_extension_ad_blocking_enabled" - case .webExtensionAdBlockingDisabled: return "m_web_extension_ad_blocking_disabled" case .webExtensionScriptletFetchSuccess: return "m_web_extension_scriptlet_fetch_success" case .webExtensionScriptletFetchError: return "m_web_extension_scriptlet_fetch_error" @@ -3415,6 +3432,29 @@ extension Pixel.Event { case .webExtensionScriptletInstallError: return "m_web_extension_scriptlet_install_error" case .webExtensionDailyAdBlockingState: return "m_web_extension_daily_ad_blocking_state" + case .webExtensionAdBlockingSettingsOpen: return "m_web_extension_ad_blocking_settings_open" + case .webExtensionAdBlockingEnabled: return "m_web_extension_ad_blocking_enabled" + case .webExtensionAdBlockingDisabled: return "m_web_extension_ad_blocking_disabled" + + // MARK: - Fire Mode + case .fireModeNTPPromotionShown: return "m_fire-mode_ntp-promotion_shown" + case .fireModeNTPPromotionDismissed: return "m_fire-mode_ntp-promotion_dismissed" + case .fireModeNTPPromotionEngaged: return "m_fire-mode_ntp-promotion_engaged" + case .fireModeMenuPromotionShown: return "m_fire-mode_menu-promotion_shown" + case .fireModeMenuPromotionEngaged: return "m_fire-mode_menu-promotion_engaged" + case .browsingModeSwitched: return "m_browsing-mode_switched" + case .tabSwitcherModeToggled: return "m_tab-switcher_mode-toggled" + case .fireModeBurnExecuted: return "m_fire-mode_burn_executed" + case .normalModeBurnExecuted: return "m_normal-mode_burn_executed" + case .fireModeDataCleared: return "m_fire-mode_data-cleared" + case .normalModeDataCleared: return "m_normal-mode_data-cleared" + case .fireModeLastTabClosedBurn: return "m_fire-mode_last-tab-closed_burn" + case .fireModeEmptyStateNewTab: return "m_fire-mode_empty-state_new-tab" + case .linkLongPressMenuShown: return "m_link-long-press_menu-shown" + case .linkLongPressNewTab: return "m_link-long-press_new-tab" + case .linkLongPressBackgroundTab: return "m_link-long-press_background-tab" + case .linkLongPressFireTab: return "m_link-long-press_fire-tab" + } } } diff --git a/iOS/DuckDuckGo/BrowsingMode.swift b/iOS/DuckDuckGo/BrowsingMode.swift index 677c5344a6e..22684c25a9d 100644 --- a/iOS/DuckDuckGo/BrowsingMode.swift +++ b/iOS/DuckDuckGo/BrowsingMode.swift @@ -29,4 +29,19 @@ public enum BrowsingMode: Int, CaseIterable { return false } } + + var pixelParamValue: String { + switch self { + case .fire: + return "fire" + case .normal: + return "normal" + } + } +} + +extension Tab { + var pixelParamValue: String { + return mode.pixelParamValue + } } diff --git a/iOS/DuckDuckGo/Fire/FireExecutor.swift b/iOS/DuckDuckGo/Fire/FireExecutor.swift index 42b4e443689..e8172c12bcb 100644 --- a/iOS/DuckDuckGo/Fire/FireExecutor.swift +++ b/iOS/DuckDuckGo/Fire/FireExecutor.swift @@ -55,6 +55,9 @@ struct FireRequest { enum Scope { case tab(viewModel: TabViewModel) case fireMode + // Burns only normal-mode tabs/data, leaving fire-mode tabs/data untouched. + // This differs from `.all`, which burns both fire-mode and normal-mode tabs/data together. + // Note: this case is not currently invoked in production code paths. case normalMode case all } @@ -400,9 +403,10 @@ class FireExecutor: FireExecuting { switch scope { case .tab: return TimedPixel(.singleTabDataCleared) - case .fireMode, .normalMode: - // TODO: - return new pixel - return nil + case .fireMode: + return TimedPixel(.fireModeDataCleared) + case .normalMode: + return TimedPixel(.normalModeDataCleared) case .all: return TimedPixel(.forgetAllDataCleared) } @@ -416,7 +420,8 @@ class FireExecutor: FireExecuting { let tabType = viewModel.tab.isAITab ? "ai" : "web" return [ PixelParameters.tabType: tabType, - PixelParameters.domainsCount: "\(domains?.count ?? 0)" + PixelParameters.domainsCount: "\(domains?.count ?? 0)", + PixelParameters.browsingMode: viewModel.tab.pixelParamValue ] case .fireMode: tabsModel = self.tabManager.tabsModel(for: .fire) diff --git a/iOS/DuckDuckGo/FireMode/FireModePromotionsCoordinator.swift b/iOS/DuckDuckGo/FireMode/FireModePromotionsCoordinator.swift index b660f9763a0..1eec9bad0be 100644 --- a/iOS/DuckDuckGo/FireMode/FireModePromotionsCoordinator.swift +++ b/iOS/DuckDuckGo/FireMode/FireModePromotionsCoordinator.swift @@ -114,17 +114,17 @@ final class FireModePromotionsCoordinator: FireModePromotionCoordinating { if firstSeenDate == nil { firstSeenDate = Date() } - // TODO: fire promotion shown pixel + DailyPixel.fireDailyAndCount(pixel: .fireModeNTPPromotionShown) } func markNTPPromotionDismissed() { isDismissed = true - // TODO: fire promotion dismissed pixel + Pixel.fire(pixel: .fireModeNTPPromotionDismissed) } func markNTPPromotionEngaged() { isEngaged = true - // TODO: fire promotion engaged pixel + Pixel.fire(pixel: .fireModeNTPPromotionEngaged) } // MARK: - Menu Promotion @@ -153,12 +153,12 @@ final class FireModePromotionsCoordinator: FireModePromotionCoordinating { menuPromotionFirstShownDate = Date() } menuPromotionShownCount += 1 - // TODO: fire menu promotion shown pixel + DailyPixel.fireDailyAndCount(pixel: .fireModeMenuPromotionShown) } func markMenuPromotionEngaged() { menuPromotionEngaged = true - // TODO: fire menu promotion engaged pixel + Pixel.fire(pixel: .fireModeMenuPromotionEngaged) } // MARK: - Private diff --git a/iOS/DuckDuckGo/MainViewController.swift b/iOS/DuckDuckGo/MainViewController.swift index c615afeb7e7..c68aad7d5cf 100644 --- a/iOS/DuckDuckGo/MainViewController.swift +++ b/iOS/DuckDuckGo/MainViewController.swift @@ -1585,6 +1585,7 @@ class MainViewController: UIViewController { let bucket = HomePageDisplayDailyPixelBucket(favoritesCount: favoritesCount) DailyPixel.fire(pixel: .newTabPageDisplayedDaily, withAdditionalParameters: [ "FavoriteCount": bucket.value, + PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue ]) } @@ -1614,8 +1615,9 @@ class MainViewController: UIViewController { ) } - Pixel.fire(pixel: .forgetAllPressedBrowsing) - DailyPixel.fire(pixel: .forgetAllPressedBrowsingDaily) + let browsingModeParam = [PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue] + Pixel.fire(pixel: .forgetAllPressedBrowsing, withAdditionalParameters: browsingModeParam) + DailyPixel.fire(pixel: .forgetAllPressedBrowsingDaily, withAdditionalParameters: browsingModeParam) performActionIfAITab { DailyPixel.fireDailyAndCount(pixel: .aiChatFireButtonTapped) } @@ -3151,8 +3153,8 @@ class MainViewController: UIViewController { action() } - func navigateToFireMode() { - tabManager.setBrowsingMode(.fire) + func navigateToFireMode(source: FireModeSwitchSource) { + tabManager.setBrowsingMode(.fire, source: source) showTabSwitcher() } } @@ -3542,14 +3544,15 @@ extension MainViewController: OmniBarDelegate { tab.didLaunchBrowsingMenu() + let modeParam = [PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue] switch context { case .newTabPage: - Pixel.fire(pixel: .browsingMenuOpenedNewTabPage) + Pixel.fire(pixel: .browsingMenuOpenedNewTabPage, withAdditionalParameters: modeParam) case .aiChatTab: - Pixel.fire(pixel: .browsingMenuOpened) + Pixel.fire(pixel: .browsingMenuOpened, withAdditionalParameters: modeParam) DailyPixel.fireDailyAndCount(pixel: .aiChatSettingsMenuOpened) case .website: - Pixel.fire(pixel: .browsingMenuOpened) + Pixel.fire(pixel: .browsingMenuOpened, withAdditionalParameters: modeParam) if tab.isError { Pixel.fire(pixel: .browsingMenuOpenedError) @@ -3706,16 +3709,20 @@ extension MainViewController: OmniBarDelegate { aiChat: .keyboardGoWhileOnAIChat) } - func fireControllerAwarePixel(ntp: Pixel.Event, serp: Pixel.Event, website: Pixel.Event, aiChat: Pixel.Event) { + func fireControllerAwarePixel(ntp: Pixel.Event, + serp: Pixel.Event, + website: Pixel.Event, + aiChat: Pixel.Event, + additionalParameters: [String: String] = [:]) { if newTabPageViewController != nil { - Pixel.fire(pixel: ntp) + Pixel.fire(pixel: ntp, withAdditionalParameters: additionalParameters) } else if let currentTab { if currentTab.isAITab == true { - Pixel.fire(pixel: aiChat) + Pixel.fire(pixel: aiChat, withAdditionalParameters: additionalParameters) } else if currentTab.url?.isDuckDuckGoSearch == true { - Pixel.fire(pixel: serp) + Pixel.fire(pixel: serp, withAdditionalParameters: additionalParameters) } else { - Pixel.fire(pixel: website) + Pixel.fire(pixel: website, withAdditionalParameters: additionalParameters) } } } @@ -3796,22 +3803,22 @@ extension MainViewController: OmniBarDelegate { } private func newTabShortcutAction() { - Pixel.fire(pixel: .tabSwitchLongPressNewTab) + Pixel.fire(pixel: .tabSwitchLongPressNewTab, withAdditionalParameters: [ + PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue + ]) guard !experimentDuckAIFireOnboardingFlow.controlsLocked else { return } performCancel() newTab() } private func newFireTabLongPressMenuAction() { - // TODO: - Add pixel - tabManager.setBrowsingMode(.fire) + tabManager.setBrowsingMode(.fire, source: .longPressTabsIcon) performCancel() newTab() } - + private func newNormalTabLongPressMenuAction() { - // TODO: - Add pixel - tabManager.setBrowsingMode(.normal) + tabManager.setBrowsingMode(.normal, source: .longPressTabsIcon) performCancel() newTab() } @@ -3835,10 +3842,12 @@ extension MainViewController: OmniBarDelegate { } if tapped { + let modeParam = [PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue] fireControllerAwarePixel(ntp: .addressBarClickOnNTP, serp: .addressBarClickOnSERP, website: .addressBarClickOnWebsite, - aiChat: .addressBarClickOnAIChat) + aiChat: .addressBarClickOnAIChat, + additionalParameters: modeParam) } guard newTabPageViewController == nil else { return } @@ -4087,11 +4096,13 @@ extension MainViewController: OmniBarDelegate { // MARK: - Experimental Address Bar (pixels only) func onExperimentalAddressBarTapped() { + let modeParam = [PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue] ViewHighlighter.hideAll() fireControllerAwarePixel(ntp: .addressBarClickOnNTP, serp: .addressBarClickOnSERP, website: .addressBarClickOnWebsite, - aiChat: .addressBarClickOnAIChat) + aiChat: .addressBarClickOnAIChat, + additionalParameters: modeParam) } func onExperimentalAddressBarClearPressed() { @@ -4121,7 +4132,9 @@ extension MainViewController: OmniBarDelegate { /// Delegate method called when the omnibar branding area is tapped while in AI Chat mode. func onAIChatBrandingPressed() { ViewHighlighter.hideAll() - Pixel.fire(pixel: .addressBarClickOnAIChat) + Pixel.fire(pixel: .addressBarClickOnAIChat, withAdditionalParameters: [ + PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue + ]) viewCoordinator.omniBar.beginEditing(animated: true, forTextEntryMode: .aiChat) } @@ -4201,7 +4214,7 @@ extension MainViewController: OmniBarDelegate { } func onFireModeRequested() { - navigateToFireMode() + navigateToFireMode(source: .ntpPromotion) } func isCurrentTabFireTab() -> Bool { @@ -4327,7 +4340,7 @@ extension MainViewController: NewTabPageControllerDelegate { } func newTabPageDidRequestFireMode(_ controller: NewTabPageViewController) { - navigateToFireMode() + navigateToFireMode(source: .ntpPromotion) } } @@ -4342,7 +4355,7 @@ extension MainViewController: TabDelegate { } func tabDidRequestFireMode(tab: TabViewController) { - navigateToFireMode() + navigateToFireMode(source: .menuPromotion) } var isAIChatEnabled: Bool { @@ -4438,7 +4451,7 @@ extension MainViewController: TabDelegate { func tab(_ tab: TabViewController, didRequestNewFireTabForUrl url: URL, inheritingAttribution attribution: AdClickAttributionLogic.State?) { - tabManager.setBrowsingMode(.fire) + tabManager.setBrowsingMode(.fire, source: .longPressLink) loadUrlInNewTab(url, inheritedAttribution: attribution) } @@ -4889,8 +4902,11 @@ extension MainViewController: TabSwitcherButtonDelegate { } func showTabSwitcher(_ button: TabSwitcherButton) { - Pixel.fire(pixel: .tabBarTabSwitcherOpened) - DailyPixel.fireDaily(.tabSwitcherOpenedDaily, withAdditionalParameters: TabSwitcherOpenDailyPixel().parameters(with: tabManager.allTabsModel.tabs)) + Pixel.fire(pixel: .tabBarTabSwitcherOpened, + withAdditionalParameters: [PixelParameters.browsingMode: tabManager.currentBrowsingMode.pixelParamValue]) + var openedDailyParams = TabSwitcherOpenDailyPixel().parameters(with: tabManager.allTabsModel.tabs) + openedDailyParams[PixelParameters.browsingMode] = tabManager.currentBrowsingMode.pixelParamValue + DailyPixel.fireDaily(.tabSwitcherOpenedDaily, withAdditionalParameters: openedDailyParams) performActionIfAITab { DailyPixel.fireDailyAndCount(pixel: .aiChatTabSwitcherOpened) } @@ -5128,6 +5144,7 @@ extension MainViewController { extension MainViewController: TabManagerFireModeDelegate { func tabManagerDidCloseLastFireTab() { + DailyPixel.fireDailyAndCount(pixel: .fireModeLastTabClosedBurn) Task { let request = FireRequest(options: [.data, .aiChats], trigger: .fireModeAutoClear, @@ -5162,20 +5179,23 @@ extension MainViewController: FireExecutorDelegate { private func firePixels(for request: FireRequest) { let tabType = tabManager.viewModelForCurrentTab()?.tab.isAITab == true ? "ai" : "web" + let browsingMode = tabManager.currentBrowsingMode.pixelParamValue let params: [String: String] = [ PixelParameters.source: request.source.rawValue, - PixelParameters.tabType: tabType + PixelParameters.tabType: tabType, + PixelParameters.browsingMode: browsingMode ] - + switch request.scope { case .all: Pixel.fire(pixel: .forgetAllExecuted, withAdditionalParameters: params) DailyPixel.fire(pixel: .forgetAllExecutedDaily, withAdditionalParameters: params) case .tab: DailyPixel.fireDailyAndCount(pixel: .singleTabBurnExecuted, withAdditionalParameters: params) - case .fireMode, .normalMode: - // TODO: - Add fire mode burn pixel - break + case .fireMode: + DailyPixel.fireDailyAndCount(pixel: .fireModeBurnExecuted, withAdditionalParameters: params) + case .normalMode: + DailyPixel.fireDailyAndCount(pixel: .normalModeBurnExecuted, withAdditionalParameters: params) } } diff --git a/iOS/DuckDuckGo/NewTabPageMessagesModel.swift b/iOS/DuckDuckGo/NewTabPageMessagesModel.swift index 5789d2bd493..d7a2524b35b 100644 --- a/iOS/DuckDuckGo/NewTabPageMessagesModel.swift +++ b/iOS/DuckDuckGo/NewTabPageMessagesModel.swift @@ -94,7 +94,6 @@ final class NewTabPageMessagesModel: ObservableObject { // MARK: - Fire Mode Promotion Actions func firePromotionDidAppear() { - // TODO: fire promotion shown pixel fireModePromotionEligibility?.markNTPPromotionShown() } diff --git a/iOS/DuckDuckGo/TabManager.swift b/iOS/DuckDuckGo/TabManager.swift index 56e21403c56..70e61a25bce 100644 --- a/iOS/DuckDuckGo/TabManager.swift +++ b/iOS/DuckDuckGo/TabManager.swift @@ -49,7 +49,15 @@ protocol TabManaging { /// Closes the tab and navigates to homepage reusing an existing homepage or creating a new one @MainActor func closeTabAndNavigateToHomepage(_ tab: Tab, clearTabHistory: Bool) @MainActor func closeTabAndOpenNewChat(_ tab: Tab, clearTabHistory: Bool) - @MainActor func setBrowsingMode(_ mode: BrowsingMode) + @MainActor func setBrowsingMode(_ mode: BrowsingMode, source: FireModeSwitchSource) +} + +enum FireModeSwitchSource: String { + case tabSelection = "tab_selection" + case longPressTabsIcon = "long_press_tabs_icon" + case menuPromotion = "menu_promotion" + case ntpPromotion = "ntp_promotion" + case longPressLink = "long_press_link" } /// Receives lifecycle events for TabViewController instances managed by TabManager. @@ -246,7 +254,7 @@ class TabManager: TabManaging, TrackerAnimationSuppressing { } @MainActor - func setBrowsingMode(_ mode: BrowsingMode) { + func setBrowsingMode(_ mode: BrowsingMode, source: FireModeSwitchSource) { guard mode != currentBrowsingMode else { return } @@ -255,7 +263,10 @@ class TabManager: TabManaging, TrackerAnimationSuppressing { if mode == .fire { fireModePromotionEligibility?.markFireModeVisited() } - // TODO: - Fire pixel + Pixel.fire(pixel: .browsingModeSwitched, withAdditionalParameters: [ + PixelParameters.browsingMode: mode.pixelParamValue, + PixelParameters.source: source.rawValue + ]) } func tabsModel(for mode: BrowsingMode) -> TabsModelManaging { @@ -489,7 +500,7 @@ class TabManager: TabManaging, TrackerAnimationSuppressing { @MainActor @discardableResult func select(_ tab: Tab, dismissCurrent: Bool = true, in tabsModel: TabsModelManaging? = nil) -> TabViewController? { - setBrowsingMode(tab.mode) + setBrowsingMode(tab.mode, source: .tabSelection) let model = tabsModel ?? currentTabsModel if dismissCurrent { current()?.dismiss() diff --git a/iOS/DuckDuckGo/TabSwitcherPageViewController.swift b/iOS/DuckDuckGo/TabSwitcherPageViewController.swift index 1f3f1a569f9..9cf5a232799 100644 --- a/iOS/DuckDuckGo/TabSwitcherPageViewController.swift +++ b/iOS/DuckDuckGo/TabSwitcherPageViewController.swift @@ -172,6 +172,7 @@ class TabSwitcherPageViewController: UIViewController { private func setupFireModeEmptyState() { guard browsingMode == .fire, isFireModeEnabled else { return } let emptyStateView = FireModeEmptyStateView(type: .tabSwitcher(onNewFireTab: { [weak self] in + Pixel.fire(pixel: .fireModeEmptyStateNewTab) self?.onNewFireTab?() })) let hostingController = UIHostingController(rootView: emptyStateView) @@ -391,7 +392,9 @@ extension TabSwitcherPageViewController: UICollectionViewDelegate { pageDelegate?.page(self, didSelectTabAt: indexPath.row) } else { currentSelection = indexPath.row - Pixel.fire(pixel: .tabSwitcherSwitchTabs) + Pixel.fire(pixel: .tabSwitcherSwitchTabs, withAdditionalParameters: [ + PixelParameters.browsingMode: browsingMode.pixelParamValue + ]) if let tab = tabsModel.get(tabAt: indexPath.row) { if tab.isAITab { DailyPixel.fireDailyAndCount(pixel: .tabManagerSwitchToAITab) @@ -427,8 +430,9 @@ extension TabSwitcherPageViewController: UICollectionViewDelegate { let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in guard let self else { return nil } - Pixel.fire(pixel: .tabSwitcherLongPress) - DailyPixel.fire(pixel: .tabSwitcherLongPressDaily) + let modeParam = [PixelParameters.browsingMode: self.browsingMode.pixelParamValue] + Pixel.fire(pixel: .tabSwitcherLongPress, withAdditionalParameters: modeParam) + DailyPixel.fire(pixel: .tabSwitcherLongPressDaily, withAdditionalParameters: modeParam) return self.pageDelegate?.page(self, contextMenuForTabsAt: indexPaths) } return configuration diff --git a/iOS/DuckDuckGo/TabSwitcherViewController+MultiSelect.swift b/iOS/DuckDuckGo/TabSwitcherViewController+MultiSelect.swift index e6283aad82c..c38138326b2 100644 --- a/iOS/DuckDuckGo/TabSwitcherViewController+MultiSelect.swift +++ b/iOS/DuckDuckGo/TabSwitcherViewController+MultiSelect.swift @@ -119,8 +119,9 @@ extension TabSwitcherViewController { ) } - Pixel.fire(pixel: .forgetAllPressedTabSwitching) - DailyPixel.fire(pixel: .forgetAllPressedTabSwitcherDaily) + let browsingModeParam = [PixelParameters.browsingMode: selectedBrowsingMode.pixelParamValue] + Pixel.fire(pixel: .forgetAllPressedTabSwitching, withAdditionalParameters: browsingModeParam) + DailyPixel.fire(pixel: .forgetAllPressedTabSwitcherDaily, withAdditionalParameters: browsingModeParam) ViewHighlighter.hideAll() presentFireConfirmation() } diff --git a/iOS/DuckDuckGo/TabSwitcherViewController.swift b/iOS/DuckDuckGo/TabSwitcherViewController.swift index 2352546b2ce..342fe81b04d 100644 --- a/iOS/DuckDuckGo/TabSwitcherViewController.swift +++ b/iOS/DuckDuckGo/TabSwitcherViewController.swift @@ -100,6 +100,7 @@ class TabSwitcherViewController: UIViewController { private var normalPageContainer: UIView! private(set) var firePageController: TabSwitcherPageViewController? private(set) var normalPageController: TabSwitcherPageViewController! + private var modeChangeFromSwipe = false var activePageController: TabSwitcherPageViewController { if selectedBrowsingMode == .fire, let firePageController { @@ -266,7 +267,13 @@ class TabSwitcherViewController: UIViewController { guard newMode != selectedBrowsingMode else { return } + let source = modeChangeFromSwipe ? "swipe" : "tap" + modeChangeFromSwipe = false selectedBrowsingMode = newMode + Pixel.fire(pixel: .tabSwitcherModeToggled, withAdditionalParameters: [ + PixelParameters.browsingMode: newMode.pixelParamValue, + PixelParameters.source: source + ]) syncPagingScrollViewToCurrentMode(animated: true) scrollToInitialTab() updateUIForSelectionMode() @@ -609,7 +616,9 @@ class TabSwitcherViewController: UIViewController { // Will be dismissed, so no need to process incoming updates canUpdateCollection = false - Pixel.fire(pixel: .tabSwitcherNewTab) + Pixel.fire(pixel: .tabSwitcherNewTab, withAdditionalParameters: [ + PixelParameters.browsingMode: selectedBrowsingMode.pixelParamValue + ]) dismissIfPossible(forceDismissOnEmpty: true) // This call needs to be after the dismiss to allow OmniBarEditingStateViewController // to present on top of MainVC instead of TabSwitcher. @@ -692,7 +701,7 @@ class TabSwitcherViewController: UIViewController { let tabsModel = tabManager.tabsModel(for: selectedBrowsingMode) if selectedBrowsingMode.allowsEmpty && tabsModel.isEmpty { - tabManager.setBrowsingMode(selectedBrowsingMode) + tabManager.setBrowsingMode(selectedBrowsingMode, source: .tabSelection) } else { let selectedTab = activePageController.selectedTab delegate?.tabSwitcher(self, didFinishWithSelectedTab: selectedTab) @@ -745,6 +754,7 @@ extension TabSwitcherViewController: UIScrollViewDelegate { let newMode: BrowsingMode = currentPage == 0 ? .fire : .normal if newMode != selectedBrowsingMode { + modeChangeFromSwipe = true pickerViewModel.selectItem(pickerItems[newMode.rawValue]) } } diff --git a/iOS/DuckDuckGo/TabViewCell.swift b/iOS/DuckDuckGo/TabViewCell.swift index e1c06ef6bc4..1ee248400d5 100644 --- a/iOS/DuckDuckGo/TabViewCell.swift +++ b/iOS/DuckDuckGo/TabViewCell.swift @@ -397,7 +397,9 @@ class TabViewCell: UICollectionViewCell { private func startRemoveAnimation() { self.isDeleting = true - Pixel.fire(pixel: .tabSwitcherSwipeCloseTab) + Pixel.fire(pixel: .tabSwitcherSwipeCloseTab, withAdditionalParameters: [ + PixelParameters.browsingMode: isFireTab ? BrowsingMode.fire.pixelParamValue : BrowsingMode.normal.pixelParamValue + ]) self.deleteTab() UIView.animate(withDuration: Constants.swipeAnimationDuration, animations: { self.transform = CGAffineTransform.identity.translatedBy(x: -self.frame.width, y: 0) @@ -424,7 +426,9 @@ class TabViewCell: UICollectionViewCell { } @objc func deleteTab() { - Pixel.fire(pixel: .tabSwitcherClickCloseTab) + Pixel.fire(pixel: .tabSwitcherClickCloseTab, withAdditionalParameters: [ + PixelParameters.browsingMode: isFireTab ? BrowsingMode.fire.pixelParamValue : BrowsingMode.normal.pixelParamValue + ]) closeTab() } diff --git a/iOS/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift b/iOS/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift index 3310c512b5a..814e21e4a3e 100644 --- a/iOS/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift +++ b/iOS/DuckDuckGo/TabViewControllerLongPressMenuExtension.swift @@ -30,6 +30,8 @@ extension TabViewController { func buildLinkPreviewMenu(for url: URL, withProvided providedElements: [UIMenuElement]) -> UIMenu { let isFireTab = tabModel.fireTab + let browsingModeParam = [PixelParameters.browsingMode: tabModel.pixelParamValue] + Pixel.fire(pixel: .linkLongPressMenuShown, withAdditionalParameters: browsingModeParam) var sections = [UIMenuElement]() var tabActions = [UIMenuElement]() @@ -74,6 +76,9 @@ extension TabViewController { } private func onNewTabAction(url: URL) { + Pixel.fire(pixel: .linkLongPressNewTab, withAdditionalParameters: [ + PixelParameters.browsingMode: tabModel.pixelParamValue + ]) delegate?.tab(self, didRequestNewTabForUrl: url, openedByPage: false, @@ -81,12 +86,16 @@ extension TabViewController { } private func onFireTabAction(url: URL) { + Pixel.fire(pixel: .linkLongPressFireTab) delegate?.tab(self, didRequestNewFireTabForUrl: url, inheritingAttribution: adClickAttributionLogic.state) } private func onBackgroundTabAction(url: URL) { + Pixel.fire(pixel: .linkLongPressBackgroundTab, withAdditionalParameters: [ + PixelParameters.browsingMode: tabModel.pixelParamValue + ]) delegate?.tab(self, didRequestNewBackgroundTabForUrl: url, inheritingAttribution: adClickAttributionLogic.state) } diff --git a/iOS/DuckDuckGo/TabViewControllerMenuBuilderExtension.swift b/iOS/DuckDuckGo/TabViewControllerMenuBuilderExtension.swift index 482742d5218..8c991ea6b4f 100644 --- a/iOS/DuckDuckGo/TabViewControllerMenuBuilderExtension.swift +++ b/iOS/DuckDuckGo/TabViewControllerMenuBuilderExtension.swift @@ -386,12 +386,16 @@ extension TabViewController { } private func onNewTabAction() { - Pixel.fire(pixel: .browsingMenuNewTab) + Pixel.fire(pixel: .browsingMenuNewTab, withAdditionalParameters: [ + PixelParameters.browsingMode: BrowsingMode.normal.pixelParamValue + ]) delegate?.tabDidRequestNewTab(self) } - + private func onNewFireTabAction() { - // TODO: - Add pixel + Pixel.fire(pixel: .browsingMenuNewTab, withAdditionalParameters: [ + PixelParameters.browsingMode: BrowsingMode.fire.pixelParamValue + ]) delegate?.tabDidRequestNewTab(self) } @@ -983,7 +987,6 @@ extension TabViewController: BrowsingMenuEntryBuilding { detailBadge: UserText.fireModeMenuPromotionBadge) { [weak self] in self?.fireModePromotionCoordinator?.markMenuPromotionEngaged() guard let self else { return } - // TODO: fire menu promotion engaged pixel self.delegate?.tabDidRequestFireMode(tab: self) } } diff --git a/iOS/DuckDuckGo/TabsBarViewController.swift b/iOS/DuckDuckGo/TabsBarViewController.swift index a5c8e06bbd9..50ffbaf3e64 100644 --- a/iOS/DuckDuckGo/TabsBarViewController.swift +++ b/iOS/DuckDuckGo/TabsBarViewController.swift @@ -463,12 +463,12 @@ extension MainViewController: TabsBarDelegate { } func tabsBarDidRequestNewFireTab(_ controller: TabsBarViewController) { - tabManager.setBrowsingMode(.fire) + tabManager.setBrowsingMode(.fire, source: .longPressTabsIcon) newTab() } - + func tabsBarDidRequestNewNormalTab(_ controller: TabsBarViewController) { - tabManager.setBrowsingMode(.normal) + tabManager.setBrowsingMode(.normal, source: .longPressTabsIcon) newTab() } diff --git a/iOS/DuckDuckGo/UnifiedToggleInput/MainViewController+UnifiedToggleInput.swift b/iOS/DuckDuckGo/UnifiedToggleInput/MainViewController+UnifiedToggleInput.swift index 1ca3374c425..e080e157539 100644 --- a/iOS/DuckDuckGo/UnifiedToggleInput/MainViewController+UnifiedToggleInput.swift +++ b/iOS/DuckDuckGo/UnifiedToggleInput/MainViewController+UnifiedToggleInput.swift @@ -652,7 +652,7 @@ extension MainViewController: UnifiedInputContentContainerViewControllerDelegate func unifiedInputEditingStateDidRequestFireMode() { unifiedToggleInputCoordinator?.contentViewController.dismissAnimated() - navigateToFireMode() + navigateToFireMode(source: .ntpPromotion) } func unifiedInputEditingStateDidChangeMode(_ mode: TextEntryMode) { diff --git a/iOS/DuckDuckGoTests/TabManagerTests.swift b/iOS/DuckDuckGoTests/TabManagerTests.swift index 31834f80c4f..ce5e2d43fde 100644 --- a/iOS/DuckDuckGoTests/TabManagerTests.swift +++ b/iOS/DuckDuckGoTests/TabManagerTests.swift @@ -143,7 +143,7 @@ final class TabManagerTests: XCTestCase { flagger.enabledFeatureFlags = [.fireMode] let manager = try makeManager(tabsModel, featureFlagger: flagger) - manager.setBrowsingMode(.fire) + manager.setBrowsingMode(.fire, source: .tabSelection) XCTAssertEqual(manager.currentBrowsingMode, .fire) // Simulate the feature flag source changing mid-session; @@ -165,7 +165,7 @@ final class TabManagerTests: XCTestCase { let flagger = MockFeatureFlagger() flagger.enabledFeatureFlags = [.fireMode] let manager = try makeManager(normalModel, fireModel: fireModel, featureFlagger: flagger) - manager.setBrowsingMode(.fire) + manager.setBrowsingMode(.fire, source: .tabSelection) XCTAssertEqual(manager.currentTabsModel.count, 2) @@ -181,7 +181,7 @@ final class TabManagerTests: XCTestCase { let flagger = MockFeatureFlagger() flagger.enabledFeatureFlags = [.fireMode] let manager = try makeManager(normalModel, fireModel: fireModel, featureFlagger: flagger) - manager.setBrowsingMode(.fire) + manager.setBrowsingMode(.fire, source: .tabSelection) XCTAssertEqual(manager.currentTabsModel.count, 0) XCTAssertNil(manager.current(createIfNeeded: false)) @@ -194,7 +194,7 @@ final class TabManagerTests: XCTestCase { let flagger = MockFeatureFlagger() flagger.enabledFeatureFlags = [.fireMode] let manager = try makeManager(normalModel, fireModel: fireModel, featureFlagger: flagger) - manager.setBrowsingMode(.fire) + manager.setBrowsingMode(.fire, source: .tabSelection) XCTAssertEqual(manager.currentTabsModel.count, 1) @@ -211,7 +211,7 @@ final class TabManagerTests: XCTestCase { let flagger = MockFeatureFlagger() flagger.enabledFeatureFlags = [.fireMode] let manager = try makeManager(normalModel, fireModel: fireModel, featureFlagger: flagger) - manager.setBrowsingMode(.fire) + manager.setBrowsingMode(.fire, source: .tabSelection) let newTab = Tab(fireTab: true) manager.replace(tab: oldTab, withNewTab: newTab) diff --git a/iOS/PixelDefinitions/pixels/definitions/address_bar.json5 b/iOS/PixelDefinitions/pixels/definitions/address_bar.json5 index 69b9135bac5..aefdb3ba5ea 100644 --- a/iOS/PixelDefinitions/pixels/definitions/address_bar.json5 +++ b/iOS/PixelDefinitions/pixels/definitions/address_bar.json5 @@ -12,7 +12,7 @@ "owners": ["aataraxiaa"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion"] + "parameters": ["appVersion", "browsingMode"] }, "m_addressbar_focus_clear_entry_aichat": { "description": "Fires when user clears address bar entry while on AI Chat", diff --git a/iOS/PixelDefinitions/pixels/definitions/fire-mode.json5 b/iOS/PixelDefinitions/pixels/definitions/fire-mode.json5 new file mode 100644 index 00000000000..49a923c4453 --- /dev/null +++ b/iOS/PixelDefinitions/pixels/definitions/fire-mode.json5 @@ -0,0 +1,94 @@ +{ + // MARK: - Promotion Pixels + + "m_fire-mode_ntp-promotion_shown": { + "description": "Fired when the Fire Mode NTP promotion card is displayed to the user", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["first_daily_count", "platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + }, + + "m_fire-mode_ntp-promotion_dismissed": { + "description": "Fired when the user dismisses the Fire Mode NTP promotion card", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + }, + + "m_fire-mode_ntp-promotion_engaged": { + "description": "Fired when the user taps the CTA on the Fire Mode NTP promotion card", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + }, + + "m_fire-mode_menu-promotion_shown": { + "description": "Fired when the Fire Mode promotion row is displayed in the browsing menu", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["first_daily_count", "platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + }, + + "m_fire-mode_menu-promotion_engaged": { + "description": "Fired when the user taps the Fire Mode promotion row in the browsing menu", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + }, + + // MARK: - Mode Switching + + "m_browsing-mode_switched": { + "description": "Fired when the browsing mode actually changes (on tab switcher dismiss, long press, promotions, etc.)", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": [ + "appVersion", + "browsingMode", + { + "key": "source", + "description": "The entry point that triggered the mode switch", + "type": "string", + "enum": ["tab_selection", "long_press_tabs_icon", "menu_promotion", "ntp_promotion", "long_press_link"] + } + ] + }, + + "m_tab-switcher_mode-toggled": { + "description": "Fired when the user toggles or swipes between fire and normal modes in the tab switcher UI", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": [ + "appVersion", + "browsingMode", + { + "key": "source", + "description": "How the user toggled the mode", + "type": "string", + "enum": ["tap", "swipe"] + } + ] + }, + + // MARK: - Empty State + + "m_fire-mode_empty-state_new-tab": { + "description": "Fired when the user taps 'New Fire Tab' from the Fire Mode empty state in the tab switcher", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion"] + } +} diff --git a/iOS/PixelDefinitions/pixels/definitions/forget_all.json5 b/iOS/PixelDefinitions/pixels/definitions/forget_all.json5 index 33ff7b08232..d3558250d97 100644 --- a/iOS/PixelDefinitions/pixels/definitions/forget_all.json5 +++ b/iOS/PixelDefinitions/pixels/definitions/forget_all.json5 @@ -4,14 +4,14 @@ "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion"] + "parameters": ["appVersion", "browsingMode"] }, "mf_tp": { "description": "Fired when user pressed the Fire button in the tab switcher", "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion"] + "parameters": ["appVersion", "browsingMode"] }, "m_forget-all-pressed_settings": { "description": "Fired when user pressed the button to clear data in Data Clearing Settings", @@ -25,7 +25,7 @@ "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion", "burnSource", "tabType"] + "parameters": ["appVersion", "burnSource", "tabType", "browsingMode"] }, "mf_dc": { "description": "Fired when data clearing is completed after Fire button action, includes timing information", @@ -34,11 +34,7 @@ "suffixes": ["platform", "form_factor"], "parameters": [ "appVersion", - { - "key": "dur", - "type": "string", - "description": "Duration in seconds (as a floating-point string)" - }, + "durationSeconds", { "key": "tc", "type": "string", @@ -51,21 +47,21 @@ "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion"] + "parameters": ["appVersion", "browsingMode"] }, "m_forget-all-pressed_tab-switcher_daily": { "description": "Fired daily when user pressed the Fire button in the tab switcher", "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion"] + "parameters": ["appVersion", "browsingMode"] }, "m_forget-all-executed_daily": { "description": "Fired daily when user confirmed the Fire button action and data clearing is executed", "owners": ["dus7", "hassaanelgarem"], "triggers": ["other"], "suffixes": ["platform", "form_factor"], - "parameters": ["appVersion", "burnSource", "tabType"] + "parameters": ["appVersion", "burnSource", "tabType", "browsingMode"] }, "m_automatic_data_clearing_options_updated": { "description": "Fired when user updates the automatic data clearing options (tabs, data, chats toggles) in Settings", @@ -77,20 +73,17 @@ { "key": "tabs", "description": "Whether tabs clearing is enabled", - "type": "string", - "enum": ["true", "false"] + "type": "boolean" }, { "key": "data", "description": "Whether data clearing is enabled", - "type": "string", - "enum": ["true", "false"] + "type": "boolean" }, { "key": "chats", "description": "Whether Duck.ai chats clearing is enabled", - "type": "string", - "enum": ["true", "false"] + "type": "boolean" } ] }, @@ -99,7 +92,7 @@ "owners": ["hassaanelgarem"], "triggers": ["other"], "suffixes": ["first_daily_count", "platform", "form_factor"], - "parameters": ["appVersion", "burnSource", "tabType"] + "parameters": ["appVersion", "burnSource", "tabType", "browsingMode"] }, "m_single-tab-data_cleared": { "description": "Fired when single tab data clearing completes, includes timing information", @@ -109,16 +102,69 @@ "parameters": [ "appVersion", "tabType", - { - "key": "dur", - "type": "string", - "description": "Duration in seconds (as a floating-point string)" - }, + "browsingMode", + "durationSeconds", { "key": "domainsCount", "type": "string", "description": "Number of domains cleared" } ] + }, + + "m_fire-mode_burn_executed": { + "description": "Fired when the fire button burns Fire Mode data only", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["first_daily_count", "platform", "form_factor"], + "parameters": ["appVersion", "burnSource", "tabType", "browsingMode"] + }, + + "m_normal-mode_burn_executed": { + "description": "Fired when the fire button burns Normal Mode data only", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["first_daily_count", "platform", "form_factor"], + "parameters": ["appVersion", "burnSource", "tabType", "browsingMode"] + }, + + "m_fire-mode_data-cleared": { + "description": "Fired when Fire Mode data clearing completes, includes timing information", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["platform", "form_factor"], + "parameters": [ + "appVersion", + "durationSeconds", + { + "key": "tc", + "type": "string", + "description": "Tab count at time of firing" + } + ] + }, + + "m_normal-mode_data-cleared": { + "description": "Fired when Normal Mode data clearing completes, includes timing information", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["platform", "form_factor"], + "parameters": [ + "appVersion", + "durationSeconds", + { + "key": "tc", + "type": "string", + "description": "Tab count at time of firing" + } + ] + }, + + "m_fire-mode_last-tab-closed_burn": { + "description": "Fired when the last Fire Tab is closed, triggering an automatic burn of Fire Mode data", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["first_daily_count", "platform", "form_factor"], + "parameters": ["appVersion"] } } diff --git a/iOS/PixelDefinitions/pixels/definitions/link_long_press.json5 b/iOS/PixelDefinitions/pixels/definitions/link_long_press.json5 new file mode 100644 index 00000000000..82dbefae104 --- /dev/null +++ b/iOS/PixelDefinitions/pixels/definitions/link_long_press.json5 @@ -0,0 +1,36 @@ +{ + "m_link-long-press_menu-shown": { + "description": "Fired when a link long-press preview menu is displayed", + "owners": ["hassaanelgarem"], + "triggers": ["other"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion", "browsingMode"] + }, + + "m_link-long-press_new-tab": { + "description": "Fired when the user opens a link in a new tab via long-press menu", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion", "browsingMode"], + "expires": "2026-08-04" + }, + + "m_link-long-press_background-tab": { + "description": "Fired when the user opens a link in a background tab via long-press menu", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion", "browsingMode"], + "expires": "2026-08-04" + }, + + "m_link-long-press_fire-tab": { + "description": "Fired when the user opens a link in a Fire Tab via long-press menu (only available in normal mode)", + "owners": ["hassaanelgarem"], + "triggers": ["user_submitted"], + "suffixes": ["platform", "form_factor"], + "parameters": ["appVersion"], + "expires": "2026-08-04" + } +} diff --git a/iOS/PixelDefinitions/pixels/params_dictionary.json5 b/iOS/PixelDefinitions/pixels/params_dictionary.json5 index 0c65ff57058..b90c2f90e7d 100644 --- a/iOS/PixelDefinitions/pixels/params_dictionary.json5 +++ b/iOS/PixelDefinitions/pixels/params_dictionary.json5 @@ -305,5 +305,16 @@ "key": "free_trial", "type": "boolean", "description": "Whether the user is eligible for a free trial" + }, + "browsingMode": { + "key": "browsing_mode", + "type": "string", + "description": "The current browsing mode: fire (Fire Tabs) or normal", + "enum": ["fire", "normal"] + }, + "durationSeconds": { + "key": "dur", + "type": "string", + "description": "Duration in seconds (as a floating-point string)" } } diff --git a/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tabs/MockTabManager.swift b/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tabs/MockTabManager.swift index b20b00b4bc2..b0b0ec14ace 100644 --- a/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tabs/MockTabManager.swift +++ b/iOS/SharedTestUtils/Mocks/DuckDuckGo/Tabs/MockTabManager.swift @@ -115,7 +115,7 @@ class MockTabManager: TabManaging { private(set) var setBrowsingModeCalled = false private(set) var setBrowsingModeCalledWith: BrowsingMode? - func setBrowsingMode(_ mode: BrowsingMode) { + func setBrowsingMode(_ mode: BrowsingMode, source: FireModeSwitchSource) { setBrowsingModeCalled = true setBrowsingModeCalledWith = mode }