From a58b9c9da8c7b8683224143c20bf3c2463bd7c55 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 4 Dec 2024 11:48:43 -0800 Subject: [PATCH] Add `install` pixel (#3656) Task/Issue URL: https://app.asana.com/0/1199333091098016/1208873240363373/f Tech Design URL: CC: Description: This PR adds an m.install.ios.* pixel that is sent when calling the exti URL. --- Core/PixelEvent.swift | 4 +++- Core/StatisticsLoader.swift | 20 +++++++++++++++++++- DuckDuckGoTests/StatisticsLoaderTests.swift | 21 ++++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2edc84584d..48d2f36ac4 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -27,7 +27,8 @@ import NetworkProtection extension Pixel { public enum Event { - + + case appInstall case appLaunch case refreshPressed case pullToRefresh @@ -914,6 +915,7 @@ extension Pixel.Event { public var name: String { switch self { + case .appInstall: return "m_install" case .appLaunch: return "ml" case .refreshPressed: return "m_r" case .pullToRefresh: return "m_pull-to-reload" diff --git a/Core/StatisticsLoader.swift b/Core/StatisticsLoader.swift index a8001e6077..7d60430add 100644 --- a/Core/StatisticsLoader.swift +++ b/Core/StatisticsLoader.swift @@ -35,15 +35,18 @@ public class StatisticsLoader { private let parser = AtbParser() private let atbPresenceFileMarker = BoolFileMarker(name: .isATBPresent) private let inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring + private let pixelFiring: PixelFiring.Type init(statisticsStore: StatisticsStore = StatisticsUserDefaults(), returnUserMeasurement: ReturnUserMeasurement = KeychainReturnUserMeasurement(), usageSegmentation: UsageSegmenting = UsageSegmentation(), - inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor()) { + inconsistencyMonitoring: StatisticsStoreInconsistencyMonitoring = StorageInconsistencyMonitor(), + pixelFiring: PixelFiring.Type = Pixel.self) { self.statisticsStore = statisticsStore self.returnUserMeasurement = returnUserMeasurement self.usageSegmentation = usageSegmentation self.inconsistencyMonitoring = inconsistencyMonitoring + self.pixelFiring = pixelFiring } public func load(completion: @escaping Completion = {}) { @@ -94,6 +97,7 @@ public class StatisticsLoader { completion() return } + self.fireInstallPixel() self.statisticsStore.installDate = Date() self.statisticsStore.atb = atb.version self.returnUserMeasurement.installCompletedWithATB(atb) @@ -102,6 +106,20 @@ public class StatisticsLoader { } } + private func fireInstallPixel() { + let formattedLocale = Locale.current.localeIdentifierAsJsonFormat + let isReinstall = String(statisticsStore.variant == VariantIOS.returningUser.name) + let parameters = [ + "locale": formattedLocale, + "reinstall": isReinstall + ] + pixelFiring.fire(.appInstall, withAdditionalParameters: parameters, includedParameters: [.appVersion], onComplete: { error in + if let error { + Logger.general.error("Install pixel failed with error: \(error.localizedDescription, privacy: .public)") + } + }) + } + private func createATBFileMarker() { atbPresenceFileMarker?.mark() } diff --git a/DuckDuckGoTests/StatisticsLoaderTests.swift b/DuckDuckGoTests/StatisticsLoaderTests.swift index f066097e99..8fa235203c 100644 --- a/DuckDuckGoTests/StatisticsLoaderTests.swift +++ b/DuckDuckGoTests/StatisticsLoaderTests.swift @@ -27,20 +27,26 @@ class StatisticsLoaderTests: XCTestCase { var mockStatisticsStore: StatisticsStore! var mockUsageSegmentation: MockUsageSegmentation! + var mockPixelFiring: PixelFiringMock.Type! var testee: StatisticsLoader! override func setUpWithError() throws { try super.setUpWithError() + PixelFiringMock.tearDown() + + mockPixelFiring = PixelFiringMock.self mockStatisticsStore = MockStatisticsStore() mockUsageSegmentation = MockUsageSegmentation() testee = StatisticsLoader(statisticsStore: mockStatisticsStore, usageSegmentation: mockUsageSegmentation, - inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring()) + inconsistencyMonitoring: MockStatisticsStoreInconsistencyMonitoring(), + pixelFiring: mockPixelFiring) } override func tearDown() { HTTPStubs.removeAllStubs() + PixelFiringMock.tearDown() super.tearDown() } @@ -270,6 +276,19 @@ class StatisticsLoaderTests: XCTestCase { waitForExpectations(timeout: 5, handler: nil) } + func testWhenInstallStatisticsRequestedThenInstallPixelIsFired() { + loadSuccessfulExiStub() + + let testExpectation = expectation(description: "refresh complete") + testee.refreshAppRetentionAtb { + Thread.sleep(forTimeInterval: .seconds(0.1)) + testExpectation.fulfill() + } + + wait(for: [testExpectation], timeout: 5.0) + XCTAssertEqual(mockPixelFiring.lastPixelName, Pixel.Event.appInstall.name) + } + func loadSuccessfulAtbStub() { stub(condition: isHost(URL.atb.host!)) { _ in let path = OHPathForFile("MockFiles/atb.json", type(of: self))!