diff --git a/firefox-ios/Client/AdjustTelemetryHelper.swift b/firefox-ios/Client/AdjustTelemetryHelper.swift index 0db7a0578f83..5e4ff36680a0 100644 --- a/firefox-ios/Client/AdjustTelemetryHelper.swift +++ b/firefox-ios/Client/AdjustTelemetryHelper.swift @@ -54,7 +54,7 @@ // telemetry.record(network: network) // } // -// gleanWrapper.submitPing() +// gleanWrapper.submit(GleanMetrics.Pings.shared.firstSession) // } //} // swiftlint:enable comment_spacing file_header diff --git a/firefox-ios/Client/Frontend/Home/Homepage Rebuild/TopSites/TopSiteState.swift b/firefox-ios/Client/Frontend/Home/Homepage Rebuild/TopSites/TopSiteState.swift index 24a51456b069..d77e556f1a29 100644 --- a/firefox-ios/Client/Frontend/Home/Homepage Rebuild/TopSites/TopSiteState.swift +++ b/firefox-ios/Client/Frontend/Home/Homepage Rebuild/TopSites/TopSiteState.swift @@ -54,7 +54,7 @@ final class TopSiteState: Hashable, Equatable { // Only sending sponsored tile impressions for now guard let tile = site as? SponsoredTile else { return } - SponsoredTileTelemetry.sendImpressionTelemetry(tile: tile, position: position) + DefaultSponsoredTileTelemetry().sendImpressionTelemetry(tile: tile, position: position) } func getTelemetrySiteType() -> String { diff --git a/firefox-ios/Client/Frontend/Home/TopSites/DataManagement/UnifiedAds/UnifiedAdsCallbackTelemetry.swift b/firefox-ios/Client/Frontend/Home/TopSites/DataManagement/UnifiedAds/UnifiedAdsCallbackTelemetry.swift index ad1bcb478884..f412e6909b40 100644 --- a/firefox-ios/Client/Frontend/Home/TopSites/DataManagement/UnifiedAds/UnifiedAdsCallbackTelemetry.swift +++ b/firefox-ios/Client/Frontend/Home/TopSites/DataManagement/UnifiedAds/UnifiedAdsCallbackTelemetry.swift @@ -12,25 +12,30 @@ protocol UnifiedAdsCallbackTelemetry { } final class DefaultUnifiedAdsCallbackTelemetry: UnifiedAdsCallbackTelemetry { - private var networking: ContileNetworking - private var logger: Logger + private let networking: ContileNetworking + private let logger: Logger + private let sponsoredTileTelemetry: SponsoredTileTelemetry init( networking: ContileNetworking = DefaultContileNetwork(with: NetworkUtils.defaultURLSession()), - logger: Logger = DefaultLogger.shared + logger: Logger = DefaultLogger.shared, + sponsoredTileTelemetry: SponsoredTileTelemetry = DefaultSponsoredTileTelemetry() ) { self.networking = networking self.logger = logger + self.sponsoredTileTelemetry = sponsoredTileTelemetry } func sendImpressionTelemetry(tile: SponsoredTile, position: Int) { let impressionURL = tile.impressionURL sendTelemetry(urlString: impressionURL, position: position) + sendLegacyImpressionTelemetry(tile: tile, position: position) } func sendClickTelemetry(tile: SponsoredTile, position: Int) { let clickURL = tile.clickURL sendTelemetry(urlString: clickURL, position: position) + sendLegacyClickTelemetry(tile: tile, position: position) } private func sendTelemetry(urlString: String, position: Int) { @@ -67,4 +72,16 @@ final class DefaultUnifiedAdsCallbackTelemetry: UnifiedAdsCallbackTelemetry { } } } + + // MARK: Legacy telemetry + // FXIOS-11121 While we are migrating to the new Unified Ads telemetry system, we should + // keep sending the legacy telemetry Glean pings + + private func sendLegacyImpressionTelemetry(tile: SponsoredTile, position: Int) { + sponsoredTileTelemetry.sendImpressionTelemetry(tile: tile, position: position, isUnifiedAdsEnabled: true) + } + + private func sendLegacyClickTelemetry(tile: SponsoredTile, position: Int) { + sponsoredTileTelemetry.sendClickTelemetry(tile: tile, position: position, isUnifiedAdsEnabled: true) + } } diff --git a/firefox-ios/Client/Frontend/Home/TopSites/TopSite.swift b/firefox-ios/Client/Frontend/Home/TopSites/TopSite.swift index 47909051ee54..838c5b72bd71 100644 --- a/firefox-ios/Client/Frontend/Home/TopSites/TopSite.swift +++ b/firefox-ios/Client/Frontend/Home/TopSites/TopSite.swift @@ -52,17 +52,6 @@ final class TopSite: FeatureFlaggable { // MARK: Telemetry - func impressionTracking(position: Int, unifiedAdsTelemetry: UnifiedAdsCallbackTelemetry) { - // Only sending sponsored tile impressions for now - guard let tile = site as? SponsoredTile else { return } - - if featureFlags.isFeatureEnabled(.unifiedAds, checking: .buildOnly) { - unifiedAdsTelemetry.sendImpressionTelemetry(tile: tile, position: position) - } else { - SponsoredTileTelemetry.sendImpressionTelemetry(tile: tile, position: position) - } - } - func getTelemetrySiteType() -> String { if isPinned && isGoogleGUID { return "google" diff --git a/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift b/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift index d8ca692fe1d5..a0b5c0cfc25b 100644 --- a/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift +++ b/firefox-ios/Client/Frontend/Home/TopSites/TopSitesViewModel.swift @@ -33,11 +33,13 @@ class TopSitesViewModel { private let googleTopSiteManager: GoogleTopSiteManager private var wallpaperManager: WallpaperManager private let unifiedAdsTelemetry: UnifiedAdsCallbackTelemetry + private let sponsoredTileTelemetry: SponsoredTileTelemetry init(profile: Profile, isZeroSearch: Bool = false, theme: Theme, wallpaperManager: WallpaperManager, + sponsoredTileTelemetry: SponsoredTileTelemetry = DefaultSponsoredTileTelemetry(), unifiedAdsTelemetry: UnifiedAdsCallbackTelemetry = DefaultUnifiedAdsCallbackTelemetry()) { self.profile = profile self.isZeroSearch = isZeroSearch @@ -52,6 +54,7 @@ class TopSitesViewModel { topSitesDataAdaptor = adaptor self.wallpaperManager = wallpaperManager self.unifiedAdsTelemetry = unifiedAdsTelemetry + self.sponsoredTileTelemetry = sponsoredTileTelemetry adaptor.delegate = self } @@ -64,7 +67,15 @@ class TopSitesViewModel { func sendImpressionTelemetry(_ homeTopSite: TopSite, position: Int) { guard !hasSentImpressionForTile(homeTopSite) else { return } - homeTopSite.impressionTracking(position: position, unifiedAdsTelemetry: unifiedAdsTelemetry) + + // Only sending sponsored tile impressions for now + guard let tile = homeTopSite.site as? SponsoredTile else { return } + + if featureFlags.isFeatureEnabled(.unifiedAds, checking: .buildOnly) { + unifiedAdsTelemetry.sendImpressionTelemetry(tile: tile, position: position) + } else { + sponsoredTileTelemetry.sendImpressionTelemetry(tile: tile, position: position) + } } private func topSitePressTracking(homeTopSite: TopSite, position: Int) { @@ -95,7 +106,7 @@ class TopSitesViewModel { if featureFlags.isFeatureEnabled(.unifiedAds, checking: .buildOnly) { unifiedAdsTelemetry.sendClickTelemetry(tile: tile, position: position) } else { - SponsoredTileTelemetry.sendClickTelemetry(tile: tile, position: position) + sponsoredTileTelemetry.sendClickTelemetry(tile: tile, position: position) } } } diff --git a/firefox-ios/Client/Telemetry/GleanWrapper.swift b/firefox-ios/Client/Telemetry/GleanWrapper.swift index 5a1698b1ec01..b40effddb0ed 100644 --- a/firefox-ios/Client/Telemetry/GleanWrapper.swift +++ b/firefox-ios/Client/Telemetry/GleanWrapper.swift @@ -8,7 +8,6 @@ import Glean protocol GleanWrapper { func handleDeeplinkUrl(url: URL) func setUpload(isEnabled: Bool) - func submitPing() // MARK: Glean Metrics @@ -20,6 +19,7 @@ protocol GleanWrapper { func recordLabel(for metric: LabeledMetricType, label: String) func setBoolean(for metric: BooleanMetricType, value: Bool) func recordQuantity(for metric: QuantityMetricType, value: Int64) + func recordUrl(for metric: UrlMetricType, value: String) func incrementNumerator(for metric: RateMetricType, amount: Int32) func incrementDenominator(for metric: RateMetricType, amount: Int32) @@ -31,6 +31,10 @@ protocol GleanWrapper { timerId: GleanTimerId) func stopAndAccumulateTiming(for metric: TimingDistributionMetricType, timerId: GleanTimerId) + + // MARK: Pings + + func submit(ping: Ping) where ReasonCodesEnum: ReasonCodes } /// Glean wrapper to abstract Glean from our application @@ -49,10 +53,6 @@ struct DefaultGleanWrapper: GleanWrapper { glean.setCollectionEnabled(isEnabled) } - func submitPing() { - GleanMetrics.Pings.shared.firstSession.submit() - } - // MARK: Glean Metrics func recordEvent(for metric: EventMetricType, @@ -88,6 +88,10 @@ struct DefaultGleanWrapper: GleanWrapper { metric.set(value) } + func recordUrl(for metric: UrlMetricType, value: String) { + metric.set(value) + } + // MARK: RateMetricType func incrementNumerator(for metric: RateMetricType, amount: Int32) { @@ -113,4 +117,10 @@ struct DefaultGleanWrapper: GleanWrapper { timerId: GleanTimerId) { metric.stopAndAccumulate(timerId) } + + // MARK: Pings + + func submit(ping: Ping) where ReasonCodesEnum: ReasonCodes { + ping.submit() + } } diff --git a/firefox-ios/Client/Telemetry/SponsoredTileTelemetry.swift b/firefox-ios/Client/Telemetry/SponsoredTileTelemetry.swift index 7ef2a313e456..4fd7896d59a4 100644 --- a/firefox-ios/Client/Telemetry/SponsoredTileTelemetry.swift +++ b/firefox-ios/Client/Telemetry/SponsoredTileTelemetry.swift @@ -7,33 +7,89 @@ import Glean // Telemetry for the Sponsored tiles located in the Top sites on the Firefox home page // Using Pings to send the telemetry events -struct SponsoredTileTelemetry { +protocol SponsoredTileTelemetry { + func sendImpressionTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool) + func sendClickTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool) +} + +extension SponsoredTileTelemetry { + func sendImpressionTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool = false) { + sendImpressionTelemetry(tile: tile, position: position, isUnifiedAdsEnabled: isUnifiedAdsEnabled) + } + + func sendClickTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool = false) { + sendClickTelemetry(tile: tile, position: position, isUnifiedAdsEnabled: isUnifiedAdsEnabled) + } +} + +struct DefaultSponsoredTileTelemetry: SponsoredTileTelemetry { // Source is only new tab at the moment, more source could be added later static let source = "newtab" + private let gleanWrapper: GleanWrapper + + init(gleanWrapper: GleanWrapper = DefaultGleanWrapper()) { + self.gleanWrapper = gleanWrapper + } - static func sendImpressionTelemetry(tile: SponsoredTile, position: Int) { + /// Send Sponsored tile impression telemetry with Glean Pings + /// - Parameters: + /// - tile: The sponsored tile + /// - position: The position of the sponsored tile in the top sites collection view + /// - isUnifiedAdsEnabled: Whether the unified ads is enabled, if enabled some information isn't set on the ping + func sendImpressionTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool = false) { let extra = GleanMetrics.TopSites.ContileImpressionExtra( position: Int32(position), - source: SponsoredTileTelemetry.source + source: DefaultSponsoredTileTelemetry.source ) - GleanMetrics.TopSites.contileImpression.record(extra) + gleanWrapper.recordEvent(for: GleanMetrics.TopSites.contileImpression, extras: extra) - GleanMetrics.TopSites.contileTileId.set(Int64(tile.tileId)) - GleanMetrics.TopSites.contileAdvertiser.set(tile.title) - GleanMetrics.TopSites.contileReportingUrl.set(tile.impressionURL) - GleanMetrics.Pings.shared.topsitesImpression.submit() + // Some information isn't set on the ping when unified ads is enabled + if !isUnifiedAdsEnabled { + gleanWrapper.recordQuantity(for: GleanMetrics.TopSites.contileTileId, + value: Int64(tile.tileId)) + gleanWrapper.recordUrl(for: GleanMetrics.TopSites.contileReportingUrl, + value: tile.impressionURL) + } + + gleanWrapper.recordString(for: GleanMetrics.TopSites.contileAdvertiser, + value: tile.title) + gleanWrapper.submit(ping: GleanMetrics.Pings.shared.topsitesImpression) } - static func sendClickTelemetry(tile: SponsoredTile, position: Int) { + /// Send Sponsored tile click telemetry with Glean Pings + /// - Parameters: + /// - tile: The sponsored tile + /// - position: The position of the sponsored tile in the top sites collection view + /// - isUnifiedAdsEnabled: Whether the unified ads is enabled, if enabled some information isn't set on the ping + func sendClickTelemetry(tile: SponsoredTile, + position: Int, + isUnifiedAdsEnabled: Bool = false) { let extra = GleanMetrics.TopSites.ContileClickExtra( position: Int32(position), - source: SponsoredTileTelemetry.source + source: DefaultSponsoredTileTelemetry.source ) - GleanMetrics.TopSites.contileClick.record(extra) + gleanWrapper.recordEvent(for: GleanMetrics.TopSites.contileClick, extras: extra) + + // Some information isn't set on the ping when unified ads is enabled + if !isUnifiedAdsEnabled { + gleanWrapper.recordQuantity(for: GleanMetrics.TopSites.contileTileId, + value: Int64(tile.tileId)) + gleanWrapper.recordUrl(for: GleanMetrics.TopSites.contileReportingUrl, + value: tile.clickURL) + } - GleanMetrics.TopSites.contileTileId.set(Int64(tile.tileId)) - GleanMetrics.TopSites.contileAdvertiser.set(tile.title) - GleanMetrics.TopSites.contileReportingUrl.set(tile.clickURL) - GleanMetrics.Pings.shared.topsitesImpression.submit() + gleanWrapper.recordString(for: GleanMetrics.TopSites.contileAdvertiser, + value: tile.title) + gleanWrapper.submit(ping: GleanMetrics.Pings.shared.topsitesImpression) } } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Home/TopSites/UnifiedAdsCallbackTelemetryTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Home/TopSites/UnifiedAdsCallbackTelemetryTests.swift index 851d660058ec..cde8026e5b40 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Home/TopSites/UnifiedAdsCallbackTelemetryTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Home/TopSites/UnifiedAdsCallbackTelemetryTests.swift @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation +import Glean import XCTest @testable import Client @@ -10,16 +11,19 @@ import XCTest class UnifiedAdsCallbackTelemetryTests: XCTestCase { private var networking: MockContileNetworking! private var logger: MockLogger! + private var gleanWrapper: MockGleanWrapper! override func setUp() { super.setUp() networking = MockContileNetworking() logger = MockLogger() + gleanWrapper = MockGleanWrapper() } override func tearDown() { networking = nil logger = nil + gleanWrapper = nil super.tearDown() } @@ -39,10 +43,81 @@ class UnifiedAdsCallbackTelemetryTests: XCTestCase { XCTAssertEqual(logger.savedMessage, "The unified ads telemetry call failed: \(tile.clickURL)") } + func testLegacyImpressionTelemetry() throws { + let subject = createSubject() + subject.sendImpressionTelemetry(tile: tile, position: 1) + + XCTAssertEqual(gleanWrapper.recordQuantityCalled, 0) + XCTAssertEqual(gleanWrapper.recordStringCalled, 1) + XCTAssertEqual(gleanWrapper.recordUrlCalled, 0) + XCTAssertEqual(gleanWrapper.recordEventCalled, 1) + XCTAssertEqual(gleanWrapper.submitPingCalled, 1) + guard let savedPing = gleanWrapper.savedPing as? Ping else { + XCTFail("savedPing is not of type Ping") + return + } + XCTAssertEqual(asAnyHashable(savedPing), asAnyHashable(GleanMetrics.Pings.shared.topsitesImpression)) + XCTAssertEqual(gleanWrapper.savedEvents?.count, 2) + + // Ensuring we call the right metrics type + let firstSavedMetric = try XCTUnwrap( + gleanWrapper.savedEvents?[0] as? EventMetricType + ) + let expectedFirstMetricType = type(of: GleanMetrics.TopSites.contileImpression) + let firstResultMetricType = type(of: firstSavedMetric) + let debugMessage = TelemetryDebugMessage(expectedMetric: expectedFirstMetricType, + resultMetric: firstResultMetricType) + XCTAssert(firstResultMetricType == expectedFirstMetricType, debugMessage.text) + + let secondSavedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[1] as? StringMetricType) + let expectedSecondMetricType = type(of: GleanMetrics.TopSites.contileAdvertiser) + let secondResultMetricType = type(of: secondSavedMetric) + let secondDebugMessage = TelemetryDebugMessage(expectedMetric: expectedSecondMetricType, + resultMetric: secondResultMetricType) + XCTAssert(secondResultMetricType == expectedSecondMetricType, secondDebugMessage.text) + } + + func testLegacyClickTelemetry() throws { + let subject = createSubject() + subject.sendClickTelemetry(tile: tile, position: 1) + + XCTAssertEqual(gleanWrapper.recordQuantityCalled, 0) + XCTAssertEqual(gleanWrapper.recordStringCalled, 1) + XCTAssertEqual(gleanWrapper.recordUrlCalled, 0) + XCTAssertEqual(gleanWrapper.recordEventCalled, 1) + XCTAssertEqual(gleanWrapper.submitPingCalled, 1) + guard let savedPing = gleanWrapper.savedPing as? Ping else { + XCTFail("savedPing is not of type Ping") + return + } + XCTAssertEqual(asAnyHashable(savedPing), asAnyHashable(GleanMetrics.Pings.shared.topsitesImpression)) + XCTAssertEqual(gleanWrapper.savedEvents?.count, 2) + + // Ensuring we call the right metrics type + let firstSavedMetric = try XCTUnwrap( + gleanWrapper.savedEvents?[0] as? EventMetricType + ) + let expectedFirstMetricType = type(of: GleanMetrics.TopSites.contileClick) + let firstResultMetricType = type(of: firstSavedMetric) + let debugMessage = TelemetryDebugMessage(expectedMetric: expectedFirstMetricType, + resultMetric: firstResultMetricType) + XCTAssert(firstResultMetricType == expectedFirstMetricType, debugMessage.text) + + let secondSavedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[1] as? StringMetricType) + let expectedSecondMetricType = type(of: GleanMetrics.TopSites.contileAdvertiser) + let secondResultMetricType = type(of: secondSavedMetric) + let secondDebugMessage = TelemetryDebugMessage(expectedMetric: expectedSecondMetricType, + resultMetric: secondResultMetricType) + XCTAssert(secondResultMetricType == expectedSecondMetricType, secondDebugMessage.text) + } + // MARK: - Helper functions func createSubject(file: StaticString = #filePath, line: UInt = #line) -> UnifiedAdsCallbackTelemetry { - let subject = DefaultUnifiedAdsCallbackTelemetry(networking: networking, logger: logger) + let sponsoredTileTelemetry = DefaultSponsoredTileTelemetry(gleanWrapper: gleanWrapper) + let subject = DefaultUnifiedAdsCallbackTelemetry(networking: networking, + logger: logger, + sponsoredTileTelemetry: sponsoredTileTelemetry) trackForMemoryLeaks(subject, file: file, line: line) diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/Bookmarks/BookmarksTelemetryTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/Bookmarks/BookmarksTelemetryTests.swift index 90699fcd6704..a3aeb9389062 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/Bookmarks/BookmarksTelemetryTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Library/Bookmarks/BookmarksTelemetryTests.swift @@ -26,7 +26,7 @@ final class BookmarksTelemetryTests: XCTestCase { func testRecordBookmark_WhenAddedBookmark_ThenGleanIsCalled() throws { subject?.addBookmark(eventLabel: .bookmarksPanel) - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? LabeledMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? LabeledMetricType) let expectedMetricType = type(of: GleanMetrics.Bookmarks.add) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -37,7 +37,7 @@ final class BookmarksTelemetryTests: XCTestCase { func testRecordBookmark_WhenDeletedBookmark_ThenGleanIsCalled() throws { subject?.deleteBookmark(eventLabel: .bookmarksPanel) - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? LabeledMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? LabeledMetricType) let expectedMetricType = type(of: GleanMetrics.Bookmarks.delete) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -48,7 +48,7 @@ final class BookmarksTelemetryTests: XCTestCase { func testRecordBookmark_WhenOpenedSite_ThenGleanIsCalled() throws { subject?.openBookmarksSite(eventLabel: BookmarksTelemetry.EventLabel.bookmarksPanel) - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? LabeledMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? LabeledMetricType) let expectedMetricType = type(of: GleanMetrics.Bookmarks.open) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -59,7 +59,7 @@ final class BookmarksTelemetryTests: XCTestCase { func testRecordBookmark_WhenEditedSite_ThenGleanIsCalled() throws { subject?.editBookmark(eventLabel: .bookmarksPanel) - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? LabeledMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? LabeledMetricType) let expectedMetricType = type(of: GleanMetrics.Bookmarks.edit) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -70,7 +70,7 @@ final class BookmarksTelemetryTests: XCTestCase { func testRecordBookmark_WhenAddedFolder_ThenGleanIsCalled() throws { subject?.addBookmarkFolder() - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? EventMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? EventMetricType) let expectedMetricType = type(of: GleanMetrics.Bookmarks.folderAdd) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockGleanWrapper.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockGleanWrapper.swift index d93c09b2246b..79eb3545830e 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockGleanWrapper.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Mocks/MockGleanWrapper.swift @@ -8,7 +8,6 @@ import UIKit class MockGleanWrapper: GleanWrapper { var handleDeeplinkUrlCalled = 0 - var submitPingCalled = 0 var setUploadEnabledCalled = 0 var recordEventCalled = 0 var recordEventNoExtraCalled = 0 @@ -17,13 +16,16 @@ class MockGleanWrapper: GleanWrapper { var recordLabelCalled = 0 var setBooleanCalled = 0 var recordQuantityCalled = 0 + var recordUrlCalled = 0 var incrementNumeratorCalled = 0 var incrementDenominatorCalled = 0 var startTimingCalled = 0 var cancelTimingCalled = 0 var stopAndAccumulateCalled = 0 - var savedEvent: Any? + var submitPingCalled = 0 + var savedEvents: [Any]? = [] var savedExtras: Any? + var savedPing: Any? var savedHandleDeeplinkUrl: URL? var savedSetUploadIsEnabled: Bool? @@ -34,10 +36,6 @@ class MockGleanWrapper: GleanWrapper { savedHandleDeeplinkUrl = url } - func submitPing() { - submitPingCalled += 1 - } - func setUpload(isEnabled: Bool) { setUploadEnabledCalled += 1 savedSetUploadIsEnabled = isEnabled @@ -45,66 +43,76 @@ class MockGleanWrapper: GleanWrapper { func recordEvent(for metric: EventMetricType, extras: EventExtras) where ExtraObject: EventExtras { - savedEvent = metric savedExtras = extras + savedEvents?.append(metric) recordEventCalled += 1 } func recordEvent(for metric: EventMetricType) where NoExtras: EventExtras { - savedEvent = metric + savedEvents?.append(metric) recordEventNoExtraCalled += 1 } func incrementCounter(for metric: CounterMetricType) { - savedEvent = metric + savedEvents?.append(metric) incrementCounterCalled += 1 } func recordString(for metric: StringMetricType, value: String) { - savedEvent = metric + savedEvents?.append(metric) recordStringCalled += 1 } func recordLabel(for metric: LabeledMetricType, label: String) { - savedEvent = metric + savedEvents?.append(metric) recordLabelCalled += 1 } func setBoolean(for metric: BooleanMetricType, value: Bool) { - savedEvent = metric + savedEvents?.append(metric) setBooleanCalled += 1 } func recordQuantity(for metric: QuantityMetricType, value: Int64) { - savedEvent = metric + savedEvents?.append(metric) recordQuantityCalled += 1 } + func recordUrl(for metric: UrlMetricType, value: String) { + savedEvents?.append(metric) + recordUrlCalled += 1 + } + func incrementNumerator(for metric: RateMetricType, amount: Int32) { - savedEvent = metric + savedEvents?.append(metric) incrementNumeratorCalled += 1 } func incrementDenominator(for metric: RateMetricType, amount: Int32) { - savedEvent = metric + savedEvents?.append(metric) incrementDenominatorCalled += 1 } func startTiming(for metric: TimingDistributionMetricType) -> GleanTimerId { - savedEvent = metric + savedEvents?.append(metric) startTimingCalled += 1 return savedTimerId } func cancelTiming(for metric: TimingDistributionMetricType, timerId: GleanTimerId) { - savedEvent = metric + savedEvents?.append(metric) cancelTimingCalled += 1 } func stopAndAccumulateTiming(for metric: TimingDistributionMetricType, timerId: GleanTimerId) { - savedEvent = metric + savedEvents?.append(metric) stopAndAccumulateCalled += 1 } + + func submit(ping: Ping) where ReasonCodesEnum: ReasonCodes { + savedPing = ping + submitPingCalled += 1 + } } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/PrivateBrowsingTelemetryTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/PrivateBrowsingTelemetryTests.swift index a3d1757c4579..d566f568de3e 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/PrivateBrowsingTelemetryTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/PrivateBrowsingTelemetryTests.swift @@ -21,7 +21,7 @@ final class PrivateBrowsingTelemetryTests: XCTestCase { subject.sendDataClearanceTappedTelemetry(didConfirm: true) let savedEvent = try XCTUnwrap( - gleanWrapper.savedEvent as? EventMetricType + gleanWrapper.savedEvents?[0] as? EventMetricType ) let savedExtras = try XCTUnwrap( gleanWrapper.savedExtras as? GleanMetrics.PrivateBrowsing.DataClearanceIconTappedExtra @@ -41,7 +41,7 @@ final class PrivateBrowsingTelemetryTests: XCTestCase { subject.sendDataClearanceTappedTelemetry(didConfirm: false) let savedEvent = try XCTUnwrap( - gleanWrapper.savedEvent as? EventMetricType + gleanWrapper.savedEvents?[0] as? EventMetricType ) let savedExtras = try XCTUnwrap( gleanWrapper.savedExtras as? GleanMetrics.PrivateBrowsing.DataClearanceIconTappedExtra diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/SponsoredTileTelemetryTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/SponsoredTileTelemetryTests.swift index 2a8742d6e6b6..fff3f6627686 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/SponsoredTileTelemetryTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/SponsoredTileTelemetryTests.swift @@ -8,14 +8,18 @@ import Glean import XCTest class SponsoredTileTelemetryTests: XCTestCase { + private var gleanWrapper: MockGleanWrapper! + override func setUp() { super.setUp() + gleanWrapper = MockGleanWrapper() clearTest() } override func tearDown() { - super.tearDown() clearTest() + gleanWrapper = nil + super.tearDown() } // MARK: Impression @@ -25,28 +29,20 @@ class SponsoredTileTelemetryTests: XCTestCase { let contile = ContileProviderMock.defaultSuccessData[0] let topSite = SponsoredTile(contile: contile) - let expectation = expectation(description: "The top sites ping was sent") - GleanMetrics.Pings.shared.topsitesImpression.testBeforeNextSubmit { _ in - self.testEventMetricRecordingSuccess(metric: GleanMetrics.TopSites.contileImpression) - - self.testQuantityMetricSuccess(metric: GleanMetrics.TopSites.contileTileId, - expectedValue: 1, - failureMessage: "Should have contile id of \(contile.id)") - - self.testStringMetricSuccess(metric: GleanMetrics.TopSites.contileAdvertiser, - expectedValue: contile.name, - failureMessage: "Should have contile advertiser of \(contile.name)") - - self.testUrlMetricSuccess(metric: GleanMetrics.TopSites.contileReportingUrl, - expectedValue: contile.impressionUrl, - failureMessage: "Should have contile url of \(contile.impressionUrl)") - - expectation.fulfill() + let subject = createSubject() + subject.sendImpressionTelemetry(tile: topSite, position: 2) + + XCTAssertEqual(gleanWrapper.recordQuantityCalled, 1) + XCTAssertEqual(gleanWrapper.recordStringCalled, 1) + XCTAssertEqual(gleanWrapper.recordUrlCalled, 1) + XCTAssertEqual(gleanWrapper.recordEventCalled, 1) + XCTAssertEqual(gleanWrapper.submitPingCalled, 1) + guard let savedPing = gleanWrapper.savedPing as? Ping else { + XCTFail("savedPing is not of type Ping") + return } - - SponsoredTileTelemetry.sendImpressionTelemetry(tile: topSite, position: 2) - - waitForExpectations(timeout: 5.0) + XCTAssertEqual(asAnyHashable(savedPing), asAnyHashable(GleanMetrics.Pings.shared.topsitesImpression)) + XCTAssertEqual(gleanWrapper.savedEvents?.count, 4) } // MARK: Click @@ -56,62 +52,29 @@ class SponsoredTileTelemetryTests: XCTestCase { let contile = ContileProviderMock.defaultSuccessData[1] let topSite = SponsoredTile(contile: contile) - let expectation = expectation(description: "The top sites ping was sent") - GleanMetrics.Pings.shared.topsitesImpression.testBeforeNextSubmit { _ in - self.testEventMetricRecordingSuccess(metric: GleanMetrics.TopSites.contileClick) - - self.testQuantityMetricSuccess(metric: GleanMetrics.TopSites.contileTileId, - expectedValue: 2, - failureMessage: "Should have contile id of \(contile.id)") - - self.testStringMetricSuccess(metric: GleanMetrics.TopSites.contileAdvertiser, - expectedValue: contile.name, - failureMessage: "Should have contile advertiser of \(contile.name)") - - self.testUrlMetricSuccess(metric: GleanMetrics.TopSites.contileReportingUrl, - expectedValue: contile.clickUrl, - failureMessage: "Should have contile url of \(contile.clickUrl)") - expectation.fulfill() + let subject = createSubject() + subject.sendClickTelemetry(tile: topSite, position: 3) + + XCTAssertEqual(gleanWrapper.recordQuantityCalled, 1) + XCTAssertEqual(gleanWrapper.recordStringCalled, 1) + XCTAssertEqual(gleanWrapper.recordUrlCalled, 1) + XCTAssertEqual(gleanWrapper.recordEventCalled, 1) + XCTAssertEqual(gleanWrapper.submitPingCalled, 1) + guard let savedPing = gleanWrapper.savedPing as? Ping else { + XCTFail("savedPing is not of type Ping") + return } - - SponsoredTileTelemetry.sendClickTelemetry(tile: topSite, position: 3) - - waitForExpectations(timeout: 5.0) + XCTAssertEqual(asAnyHashable(savedPing), asAnyHashable(GleanMetrics.Pings.shared.topsitesImpression)) + XCTAssertEqual(gleanWrapper.savedEvents?.count, 4) } - // MARK: ContextId - func testContextIdImpressionTopSite() { - TelemetryContextualIdentifier.setupContextId() - let contile = ContileProviderMock.defaultSuccessData[0] - let topSite = SponsoredTile(contile: contile) - - let expectation = expectation(description: "The top sites ping was sent") - GleanMetrics.Pings.shared.topsitesImpression.testBeforeNextSubmit { _ in - guard let contextId = TelemetryContextualIdentifier.contextId, - let uuid = UUID(uuidString: contextId) else { - XCTFail("Expected contextId to be configured") - return - } - - self.testUuidMetricSuccess(metric: GleanMetrics.TopSites.contextId, - expectedValue: uuid, - failureMessage: "Should have contextId of \(uuid)") - expectation.fulfill() - } + // MARK: Helper methods - SponsoredTileTelemetry.sendImpressionTelemetry(tile: topSite, position: 2) - waitForExpectations(timeout: 5.0) + func createSubject() -> SponsoredTileTelemetry { + return DefaultSponsoredTileTelemetry(gleanWrapper: gleanWrapper) } - // MARK: Helper methods - func clearTest() { - // Due to changes allow certain custom pings to implement their own opt-out - // independent of Glean, custom pings may need to be registered manually in - // tests in order to puth them in a state in which they can collect data. - Glean.shared.registerPings(GleanMetrics.Pings.shared) - - Glean.shared.resetGlean(clearStores: true) TelemetryContextualIdentifier.clearUserDefaults() } } diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTray/InactiveTabsTelemetryTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTray/InactiveTabsTelemetryTests.swift index 1eac50e0e790..363567caaf58 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTray/InactiveTabsTelemetryTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/TabTray/InactiveTabsTelemetryTests.swift @@ -26,7 +26,7 @@ final class InactiveTabsTelemetryTests: XCTestCase { func testRecordInactiveTab_WhenSectionShown_ThenGleanIsCalled() throws { subject?.sectionShown() - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? CounterMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? CounterMetricType) let expectedMetricType = type(of: GleanMetrics.InactiveTabsTray.inactiveTabShown) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -37,7 +37,7 @@ final class InactiveTabsTelemetryTests: XCTestCase { func testRecordInactiveTab_WhenClosedAllTabs_ThenGleanIsCalled() throws { subject?.closedAllTabs() - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? CounterMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? CounterMetricType) let expectedMetricType = type(of: GleanMetrics.InactiveTabsTray.inactiveTabsCloseAllBtn) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -48,7 +48,7 @@ final class InactiveTabsTelemetryTests: XCTestCase { func testRecordInactiveTab_WhenTabOpened_ThenGleanIsCalled() throws { subject?.tabOpened() - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? CounterMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? CounterMetricType) let expectedMetricType = type(of: GleanMetrics.InactiveTabsTray.openInactiveTab) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -59,7 +59,7 @@ final class InactiveTabsTelemetryTests: XCTestCase { func testRecordInactiveTab_WhenTabSwipedClosed_ThenGleanIsCalled() throws { subject?.tabSwipedToClose() - let savedMetric = try XCTUnwrap(gleanWrapper.savedEvent as? CounterMetricType) + let savedMetric = try XCTUnwrap(gleanWrapper.savedEvents?[0] as? CounterMetricType) let expectedMetricType = type(of: GleanMetrics.InactiveTabsTray.inactiveTabSwipeClose) let resultMetricType = type(of: savedMetric) let debugMessage = TelemetryDebugMessage(expectedMetric: expectedMetricType, resultMetric: resultMetricType) @@ -71,7 +71,7 @@ final class InactiveTabsTelemetryTests: XCTestCase { subject?.sectionToggled(hasExpanded: true) let savedMetric = try XCTUnwrap( - gleanWrapper.savedEvent as? EventMetricType + gleanWrapper.savedEvents?[0] as? EventMetricType ) let expectedMetricType = type(of: GleanMetrics.InactiveTabsTray.toggleInactiveTabTray) let resultMetricType = type(of: savedMetric) diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/XCTestCaseExtensions.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/XCTestCaseExtensions.swift index 6417a607e63f..a71d7417e299 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/XCTestCaseExtensions.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/XCTestCaseExtensions.swift @@ -33,4 +33,9 @@ extension XCTestCase { let returnValue = try await asyncMethod() return try XCTUnwrap(returnValue, file: file, line: line) } + + /// Helper function to cast a value to `AnyHashable`. + func asAnyHashable(_ value: T) -> AnyHashable? { + return value as? AnyHashable + } }