From 89258902326d117706f1aedf6786b2f49276cafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20=C5=9Apiewak?= Date: Mon, 25 Nov 2024 16:15:11 +0100 Subject: [PATCH] Send daily pixel with open tabs count (#3587) Task/Issue URL: https://app.asana.com/0/1206226850447395/1208014614013438/f Tech Design URL: CC: **Description**: Adds a daily pixel with number of total open tabs and number of new tabs when Tab switcher is opened. **Steps to test this PR**: 1. Modify `DailyPixel.hasBeenFiredToday` to always return `false`. 2. Open a bunch of tabs including a few new tabs. 3. Check Pixel is fired when opening tab switcher containing tabs count buckets that are in line with https://app.asana.com/0/1206226850447395/1208014614013438/f. **Definition of Done (Internal Only)**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) --- Core/PixelEvent.swift | 2 + DuckDuckGo.xcodeproj/project.pbxproj | 8 ++ DuckDuckGo/MainViewController.swift | 2 + DuckDuckGo/TabSwitcherOpenDailyPixel.swift | 68 +++++++++++++ .../TabSwitcherDailyPixelTests.swift | 99 +++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 DuckDuckGo/TabSwitcherOpenDailyPixel.swift create mode 100644 DuckDuckGoTests/TabSwitcherDailyPixelTests.swift diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 85416a001c..28b764a87f 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -59,6 +59,7 @@ extension Pixel { case tabSwitcherClickCloseTab case tabSwitcherSwipeCloseTab case tabSwitchLongPressNewTab + case tabSwitcherOpenDaily case settingsDoNotSellShown case settingsDoNotSellOn @@ -910,6 +911,7 @@ extension Pixel.Event { case .tabSwitcherClickCloseTab: return "m_tab_manager_close_tab_click" case .tabSwitcherSwipeCloseTab: return "m_tab_manager_close_tab_swipe" case .tabSwitchLongPressNewTab: return "m_tab_manager_long_press_new_tab" + case .tabSwitcherOpenDaily: return "m_tab_manager_clicked_daily" case .settingsDoNotSellShown: return "ms_dns" case .settingsDoNotSellOn: return "ms_dns_on" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 61fdf33951..ed5f648507 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -366,6 +366,8 @@ 6FEC0B882C999961006B4F6E /* FavoritesListInteractingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */; }; 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */; }; 6FF9AD452CE766F700C5A406 /* NewTabPageControllerPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */; }; + 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */; }; + 6FF9AD412CE6610F00C5A406 /* TabSwitcherDailyPixelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */; }; 7B1604E82CB685B400A44EC6 /* Logger+TipKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */; }; 7B1604EC2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */; }; 7B1604EE2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */; }; @@ -1678,6 +1680,8 @@ 6FEC0B872C999961006B4F6E /* FavoritesListInteractingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapter.swift; sourceTree = ""; }; 6FF915802B88E0750042AC87 /* AdAttributionFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionFetcherTests.swift; sourceTree = ""; }; 6FF9AD442CE766F700C5A406 /* NewTabPageControllerPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerPixelTests.swift; sourceTree = ""; }; + 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherOpenDailyPixel.swift; sourceTree = ""; }; + 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherDailyPixelTests.swift; sourceTree = ""; }; 7B1604E72CB685B400A44EC6 /* Logger+TipKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+TipKit.swift"; sourceTree = ""; }; 7B1604EB2CB68BDA00A44EC6 /* TipKitController+ConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TipKitController+ConvenienceInitializers.swift"; sourceTree = ""; }; 7B1604ED2CB68D2600A44EC6 /* TipKitDebugOptionsUIActionHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipKitDebugOptionsUIActionHandling.swift; sourceTree = ""; }; @@ -5839,6 +5843,7 @@ F1386BA21E6846320062FC3C /* TabSwitcher */ = { isa = PBXGroup; children = ( + 6FF9AD3E2CE63DC200C5A406 /* TabSwitcherOpenDailyPixel.swift */, 85DFEDF824CF3D0E00973FE7 /* TabsBarCell.swift */, 9872D204247DCAC100CEF398 /* TabPreviewsSource.swift */, 8586A10F24CCCD040049720E /* TabsBarViewController.swift */, @@ -5927,6 +5932,7 @@ F13B4BF71F18C9E800814661 /* Tabs */ = { isa = PBXGroup; children = ( + 6FF9AD402CE6610600C5A406 /* TabSwitcherDailyPixelTests.swift */, 85010503292FFB080033978F /* FireproofFaviconUpdaterTests.swift */, 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */, 984D035F24AF49160066CFB8 /* TabPreviewsSourceTests.swift */, @@ -7894,6 +7900,7 @@ C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, 4B5C462A2AF2A6E6002A4432 /* VPNIntents.swift in Sources */, + 6FF9AD3F2CE63DD800C5A406 /* TabSwitcherOpenDailyPixel.swift in Sources */, 310742A62848CD780012660B /* BackForwardMenuHistoryItem.swift in Sources */, 9FDEC7B82C9004D600C7A692 /* OnboardingIntroViewModel+Copy.swift in Sources */, 859DB8162CE6263C001F7210 /* TextZoomEditorView.swift in Sources */, @@ -8062,6 +8069,7 @@ C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, 859DB81E2CE62766001F7210 /* TextZoomTests.swift in Sources */, 1DE384E42BC41E2500871AF6 /* PixelExperimentTests.swift in Sources */, + 6FF9AD412CE6610F00C5A406 /* TabSwitcherDailyPixelTests.swift in Sources */, CBDD5DE129A6741300832877 /* MockBundle.swift in Sources */, C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index c6b5f59729..8a51278c6f 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -2549,6 +2549,8 @@ extension MainViewController: TabSwitcherButtonDelegate { func showTabSwitcher(_ button: TabSwitcherButton) { Pixel.fire(pixel: .tabBarTabSwitcherPressed) + DailyPixel.fireDaily(.tabSwitcherOpenDaily, withAdditionalParameters: TabSwitcherOpenDailyPixel().parameters(with: tabManager.model.tabs)) + performCancel() showTabSwitcher() } diff --git a/DuckDuckGo/TabSwitcherOpenDailyPixel.swift b/DuckDuckGo/TabSwitcherOpenDailyPixel.swift new file mode 100644 index 0000000000..8b6941ceb2 --- /dev/null +++ b/DuckDuckGo/TabSwitcherOpenDailyPixel.swift @@ -0,0 +1,68 @@ +// +// TabSwitcherOpenDailyPixel.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 + +struct TabSwitcherOpenDailyPixel { + func parameters(with tabs: [Tab]) -> [String: String] { + var parameters = [String: String]() + + parameters[ParameterName.tabCount] = tabCountBucket(for: tabs) + parameters[ParameterName.newTabCount] = newTabCountBucket(for: tabs) + + return parameters + } + + private func tabCountBucket(for tabs: [Tab]) -> String? { + let count = tabs.count + + switch count { + case 0: return nil + case 1...1: return "1" + case 2...5: return "2-5" + case 6...10: return "6-10" + case 11...20: return "11-20" + case 21...40: return "21-40" + case 41...60: return "41-60" + case 61...80: return "61-80" + case 81...100: return "81-100" + case 101...125: return "101-125" + case 126...150: return "126-150" + case 151...250: return "151-250" + case 251...500: return "251-500" + default: return "501+" + } + + } + + private func newTabCountBucket(for tabs: [Tab]) -> String? { + let count = tabs.count { $0.link == nil } + + switch count { + case 0...1: return "0-1" + case 2...10: return "2-10" + default: return "11+" + } + } + + private enum ParameterName { + static let tabCount = "tab_count" + static let newTabCount = "new_tab_count" + } +} diff --git a/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift b/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift new file mode 100644 index 0000000000..7cc16a6074 --- /dev/null +++ b/DuckDuckGoTests/TabSwitcherDailyPixelTests.swift @@ -0,0 +1,99 @@ +// +// TabSwitcherDailyPixelTests.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 Core +@testable import DuckDuckGo + +final class TabSwitcherDailyPixelTests: XCTestCase { + func testPopulatesParameters() { + let tabs = [Tab(), Tab(), Tab()] + let pixel = TabSwitcherOpenDailyPixel() + + let parameters = pixel.parameters(with: tabs) + + XCTAssertNotNil(parameters[ParameterName.tabCount]) + XCTAssertNotNil(parameters[ParameterName.newTabCount]) + } + + func testIncludesProperCountsForParameters() { + let tabs = [Tab(), Tab(), .mock()] + let pixel = TabSwitcherOpenDailyPixel() + + let parameters = pixel.parameters(with: tabs) + + XCTAssertEqual(parameters[ParameterName.tabCount], "2-5") + XCTAssertEqual(parameters[ParameterName.newTabCount], "2-10") + } + + func testBucketsAggregation() { + let bucketValues = [ + 1...1: "1", + 2...5: "2-5", + 6...10: "6-10", + 11...20: "11-20", + 21...40: "21-40", + 41...60: "41-60", + 61...80: "61-80", + 81...100: "81-100", + 101...125: "101-125", + 126...150: "126-150", + 151...250: "151-250", + 251...500: "251-500", + 501...504: "501+"] + + for bucket in bucketValues { + for value in bucket.key { + let tabs = Array(repeating: Tab.mock(), count: value) + + let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.tabCount] + + XCTAssertEqual(countParameter, bucket.value) + } + } + } + + func testNewTabBucketsAggregation() { + let bucketValues = [ + 0...1: "0-1", + 2...10: "2-10", + 11...20: "11+"] + + for bucket in bucketValues { + for value in bucket.key { + let tabs = Array(repeating: Tab(), count: value) + + let countParameter = TabSwitcherOpenDailyPixel().parameters(with: tabs)[ParameterName.newTabCount] + + XCTAssertEqual(countParameter, bucket.value) + } + } + } +} + +private extension Tab { + static func mock() -> Tab { + Tab(link: Link(title: nil, url: URL("https://example.com")!)) + } +} + +private enum ParameterName { + static let newTabCount = "new_tab_count" + static let tabCount = "tab_count" +}