Skip to content

Add FXIOS-12039 [Homepage] section viewed telemetry #26234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions firefox-ios/Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,8 @@
1D3822E92BAB99250046BC5E /* UIView+ThemeUUIDIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3822E82BAB99250046BC5E /* UIView+ThemeUUIDIdentifiable.swift */; };
1D3C90882ACE1AF400304C87 /* RemoteTabPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D3C90872ACE1AF400304C87 /* RemoteTabPanelTests.swift */; };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattreaganmozilla Throttler was added to the ClientTests target membership and so WindowSimpleTabsCoordinator was as well. I didn't see why they should be added so I removed it. Let me know if any issues!

image

1D4D79462BF2F4E7007C6796 /* SimpleTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E69EAF254D064E00B591C2 /* SimpleTab.swift */; };
1D4D79472BF2F4FD007C6796 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96D95015270238500079D39D /* Throttler.swift */; };
1D558A582BED7ECB001EF527 /* MockWindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D558A562BED7ECB001EF527 /* MockWindowManager.swift */; };
1D558A5A2BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D558A592BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift */; };
1D558A5B2BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D558A592BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift */; };
1D5CBF492B17E3CB0001D033 /* NotificationPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5CBF482B17E3CB0001D033 /* NotificationPayloads.swift */; };
1D5CBF4A2B17E3CB0001D033 /* NotificationPayloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5CBF482B17E3CB0001D033 /* NotificationPayloads.swift */; };
1D69FF8D27B17286001F660E /* HomeLogoHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D69FF8C27B17285001F660E /* HomeLogoHeaderCell.swift */; };
Expand Down Expand Up @@ -849,6 +847,7 @@
8A4190D22A6B0848001E8401 /* StatusBarOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4190D02A6B0843001E8401 /* StatusBarOverlayTests.swift */; };
8A4490922BF3BC2700E7E682 /* MicrosurveyPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4490912BF3BC2700E7E682 /* MicrosurveyPromptView.swift */; };
8A4490952BF3C42B00E7E682 /* MicrosurveyConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4490942BF3C42B00E7E682 /* MicrosurveyConfirmationView.swift */; };
8A44D3612DB7DFD700B7D80B /* MockThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A44D3602DB7DFD200B7D80B /* MockThrottler.swift */; };
8A44F20E2B585E1F0016BC81 /* HomepageTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A44F20D2B585E1F0016BC81 /* HomepageTelemetry.swift */; };
8A454D292CB7078D009436D9 /* PocketState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A454D282CB7078D009436D9 /* PocketState.swift */; };
8A454D2C2CB81153009436D9 /* PocketStandardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A454D2B2CB81153009436D9 /* PocketStandardCell.swift */; };
Expand Down Expand Up @@ -8083,6 +8082,7 @@
8A4190D02A6B0843001E8401 /* StatusBarOverlayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarOverlayTests.swift; sourceTree = "<group>"; };
8A4490912BF3BC2700E7E682 /* MicrosurveyPromptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MicrosurveyPromptView.swift; sourceTree = "<group>"; };
8A4490942BF3C42B00E7E682 /* MicrosurveyConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosurveyConfirmationView.swift; sourceTree = "<group>"; };
8A44D3602DB7DFD200B7D80B /* MockThrottler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockThrottler.swift; sourceTree = "<group>"; };
8A44F20D2B585E1F0016BC81 /* HomepageTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageTelemetry.swift; sourceTree = "<group>"; };
8A454D282CB7078D009436D9 /* PocketState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketState.swift; sourceTree = "<group>"; };
8A454D2B2CB81153009436D9 /* PocketStandardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketStandardCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -12441,6 +12441,7 @@
8A87B4352CC1A8FD003A9239 /* Mock */ = {
isa = PBXGroup;
children = (
8A44D3602DB7DFD200B7D80B /* MockThrottler.swift */,
8A03FFA22DB12AF700509A72 /* MockSponsoredTelemetry.swift */,
8AEF41632D15EE190013925D /* MockTopSitesManager.swift */,
8A87B4362CC1A910003A9239 /* MockPocketManager.swift */,
Expand Down Expand Up @@ -17923,6 +17924,7 @@
8A8482F02BE1602500F9007B /* MicrosurveyPromptStateTests.swift in Sources */,
8ADED7EC27691351009C19E6 /* CalendarExtensionsTests.swift in Sources */,
3B39EDBA1E16E18900EF029F /* CustomSearchEnginesTest.swift in Sources */,
8A44D3612DB7DFD700B7D80B /* MockThrottler.swift in Sources */,
8A7892072CF9228700490CA4 /* UnifiedAdsCallbackTelemetryTests.swift in Sources */,
0AFF7F662C7784F100265214 /* TrackingProtectionModelTests.swift in Sources */,
C80C11EE28B3C8B80062922A /* WallpaperMetadataTrackerTests.swift in Sources */,
Expand All @@ -17941,7 +17943,6 @@
ED07C0F52CCB020B006C0627 /* SearchEngineSelectionMiddlewareTests.swift in Sources */,
C8DF92F72A14101500AA7B05 /* OnboardingViewControllerProtocolTests.swift in Sources */,
8A4EA0D92C01127C00E4E4F1 /* MicrosurveyMockModel.swift in Sources */,
1D4D79472BF2F4FD007C6796 /* Throttler.swift in Sources */,
6A3E5D8A283831D1001E706E /* DownloadQueueTests.swift in Sources */,
8AEF41642D15EE1D0013925D /* MockTopSitesManager.swift in Sources */,
8AE80BB62891AEA100BC12EA /* MockDispatchGroup.swift in Sources */,
Expand Down Expand Up @@ -18117,7 +18118,6 @@
8A7835372D5107810052E328 /* BookmarksMiddlewareTests.swift in Sources */,
45D5EDC0292D619000311934 /* MockablePinnedSites.swift in Sources */,
5AE371852A4DD6FE0092A760 /* PasswordManagerCoordinatorDelegateMock.swift in Sources */,
1D558A5B2BEE7D07001EF527 /* WindowSimpleTabsCoordinator.swift in Sources */,
8A6E8DEB2B275BA9000C4301 /* PrivateHomepageViewControllerTests.swift in Sources */,
8A93F86529D37331004159D9 /* DefaultRouterTests.swift in Sources */,
5AD3B6802CF6674F00AFA1FE /* MockUIApplication.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ final class HomepageViewController: UIViewController,
private let overlayManager: OverlayModeManager
private let logger: Logger
private let toastContainer: UIView
private var alreadyTrackedItems = Set<HomepageItem>()

// Telemetry related
private var alreadyTrackedSections = Set<HomepageSection>()
private var alreadyTrackedTopSites = Set<HomepageItem>()
private let trackingImpressionsThrottler: ThrottleProtocol

// MARK: - Initializers
init(windowUUID: WindowUUID,
Expand All @@ -73,7 +77,8 @@ final class HomepageViewController: UIViewController,
statusBarScrollDelegate: StatusBarScrollDelegate? = nil,
toastContainer: UIView,
notificationCenter: NotificationProtocol = NotificationCenter.default,
logger: Logger = DefaultLogger.shared
logger: Logger = DefaultLogger.shared,
throttler: ThrottleProtocol = Throttler(seconds: 0.5)
) {
self.windowUUID = windowUUID
self.themeManager = themeManager
Expand All @@ -82,6 +87,7 @@ final class HomepageViewController: UIViewController,
self.statusBarScrollDelegate = statusBarScrollDelegate
self.toastContainer = toastContainer
self.logger = logger
self.trackingImpressionsThrottler = throttler

// FXIOS-11490: This should be refactored when we refactor CFR to adhere to Redux
let jumpBackInContextualViewProvider = ContextualHintViewProvider(
Expand Down Expand Up @@ -179,7 +185,7 @@ final class HomepageViewController: UIViewController,

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
alreadyTrackedItems.removeAll()
resetTrackedObjects()
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Expand Down Expand Up @@ -297,9 +303,9 @@ final class HomepageViewController: UIViewController,
)
// FXIOS-11523 - Trigger impression when user opens homepage view new tab + scroll to top
if homepageState.shouldTriggerImpression {
alreadyTrackedItems.removeAll()
trackVisibleItemImpressions()
scrollToTop()
resetTrackedObjects()
trackVisibleItemImpressions()
}
}

Expand Down Expand Up @@ -769,7 +775,7 @@ final class HomepageViewController: UIViewController,
)
}

private func dispatchOpenTopSitesAction(at index: Int, config: TopSiteConfiguration) {
private func dispatchTopSitesAction(at index: Int, config: TopSiteConfiguration, actionType: ActionType) {
let config = TopSitesTelemetryConfig(
isZeroSearch: homepageState.isZeroSearch,
position: index,
Expand All @@ -779,7 +785,7 @@ final class HomepageViewController: UIViewController,
TopSitesAction(
telemetryConfig: config,
windowUUID: self.windowUUID,
actionType: TopSitesActionType.tapOnHomepageTopSitesCell
actionType: actionType
)
)
}
Expand All @@ -804,7 +810,11 @@ final class HomepageViewController: UIViewController,
visitType: .link
)
dispatchNavigationBrowserAction(with: destination, actionType: NavigationBrowserActionType.tapOnCell)
dispatchOpenTopSitesAction(at: indexPath.item, config: config)
dispatchTopSitesAction(
at: indexPath.item,
config: config,
actionType: TopSitesActionType.tapOnHomepageTopSitesCell
)
case .jumpBackIn(let config):
store.dispatch(
JumpBackInAction(
Expand Down Expand Up @@ -867,50 +877,53 @@ final class HomepageViewController: UIViewController,
)
}

/// Want to handle tracking here to capture any cells about to viewed,
/// but some cells do not get reconfigured so we add additional tracking detection with `trackVisibleImpressions`
func collectionView(
_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath
) {
guard let item = dataSource?.itemIdentifier(for: indexPath) else { return }
handleTrackingItemImpression(with: item, at: indexPath.item)
}

/// Used to track item impressions. If the user has seen the item on the homepage, we only record the impression once.
/// Used to track impressions. If the user has already seen the item on the homepage, we only record the impression once.
/// We want to track at initial seen as well as when users scrolls.
/// A throttle is added in order to capture what the users has seen. When we scroll to top programmatically,
/// the impressions were being tracked, but to match user's perspective, we add a throttle to delay.
/// Time complexity: O(n) due to iterating visible items.
private func trackVisibleItemImpressions() {
guard let collectionView else {
logger.log(
"Homepage collectionview should not have been nil, unable to track impression",
level: .warning,
category: .homepage
)
return
}
for indexPath in collectionView.indexPathsForVisibleItems {
guard let item = dataSource?.itemIdentifier(for: indexPath) else { continue }
handleTrackingItemImpression(with: item, at: indexPath.item)
trackingImpressionsThrottler.throttle { [weak self] in
guard let self else { return }
guard let collectionView else {
logger.log(
"Homepage collectionview should not have been nil, unable to track impression",
level: .warning,
category: .homepage
)
return
}
for indexPath in collectionView.indexPathsForVisibleItems {
guard let section = dataSource?.sectionIdentifier(for: indexPath.section),
let item = dataSource?.itemIdentifier(for: indexPath) else { continue }
handleTrackingImpressions(for: section, with: item, at: indexPath.item)
}
}
}

private func handleTrackingItemImpression(with item: HomepageItem, at index: Int) {
guard !alreadyTrackedItems.contains(item) else { return }
alreadyTrackedItems.insert(item)
if case .topSite(let config, _) = item {
sendItemActionWithTelemetryExtras(
item: item,
actionType: HomepageActionType.itemSeen,
topSitesTelemetryConfig: TopSitesTelemetryConfig(
isZeroSearch: homepageState.isZeroSearch,
position: index,
topSiteConfiguration: config
)
)
} else {
sendItemActionWithTelemetryExtras(item: item, actionType: HomepageActionType.itemSeen)
}
/// We want to capture generic section impressions,
/// but we also need to handle capturing individual sponsored tiles impressions
private func handleTrackingImpressions(for section: HomepageSection, with item: HomepageItem, at index: Int) {
handleTrackingTopSitesImpression(for: item, at: index)
handleTrackingSectionImpression(for: section, with: item)
}

private func handleTrackingTopSitesImpression(for item: HomepageItem, at index: Int) {
guard !alreadyTrackedTopSites.contains(item) else { return }
alreadyTrackedTopSites.insert(item)
guard case .topSite(let config, _) = item else { return }
dispatchTopSitesAction(at: index, config: config, actionType: TopSitesActionType.topSitesSeen)
}

private func handleTrackingSectionImpression(for section: HomepageSection, with item: HomepageItem) {
guard !alreadyTrackedSections.contains(section) else { return }
alreadyTrackedSections.insert(section)
sendItemActionWithTelemetryExtras(item: item, actionType: HomepageActionType.sectionSeen)
}

private func resetTrackedObjects() {
alreadyTrackedSections.removeAll()
alreadyTrackedTopSites.removeAll()
}

// MARK: - UIPopoverPresentationControllerDelegate - Context Hints (CFR)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ enum HomepageActionType: ActionType {
case viewWillAppear
case didSelectItem
case embeddedHomepage
case itemSeen
case sectionSeen
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ final class HomepageMiddleware {
}
self.homepageTelemetry.sendItemTappedTelemetryEvent(for: type)

case HomepageActionType.itemSeen:
case HomepageActionType.sectionSeen:
guard let extras = (action as? HomepageAction)?.telemetryExtras, let type = extras.itemType else {
return
}
self.homepageTelemetry.sendItemImpressionTelemetryEvent(for: type)
self.homepageTelemetry.sendSectionLabeledCounter(for: type)

default:
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum TopSitesActionType: ActionType {
case toggleShowSectionSetting
case toggleShowSponsoredSettings
case tapOnHomepageTopSitesCell
case topSitesSeen
}

enum TopSitesMiddlewareActionType: ActionType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ final class TopSitesMiddleware: FeatureFlaggable {
case HomepageActionType.initialize:
self.getTopSitesDataAndUpdateState(for: action)

case HomepageActionType.itemSeen:
case TopSitesActionType.topSitesSeen:
self.handleSponsoredImpressionTracking(for: action)

case TopSitesActionType.fetchTopSites:
Expand Down Expand Up @@ -154,7 +154,7 @@ final class TopSitesMiddleware: FeatureFlaggable {

// MARK: Telemetry
private func handleSponsoredImpressionTracking(for action: Action) {
guard let telemetryMetadata = (action as? HomepageAction)?.telemetryExtras?.topSitesTelemetryConfig else {
guard let telemetryMetadata = (action as? TopSitesAction)?.telemetryConfig else {
self.logger.log(
"Unable to retrieve telemetryMetadata for \(action.actionType)",
level: .warning,
Expand Down
5 changes: 2 additions & 3 deletions firefox-ios/Client/Telemetry/HomepageTelemetry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ struct HomepageTelemetry {
gleanWrapper.recordEvent(for: GleanMetrics.Homepage.itemTapped, extras: itemNameExtra)
}

func sendItemImpressionTelemetryEvent(for itemType: ItemType) {
let itemNameExtra = GleanMetrics.Homepage.ItemViewedExtra(section: itemType.sectionName, type: itemType.rawValue)
gleanWrapper.recordEvent(for: GleanMetrics.Homepage.itemViewed, extras: itemNameExtra)
func sendSectionLabeledCounter(for itemType: ItemType) {
gleanWrapper.recordLabel(for: GleanMetrics.Homepage.sectionViewed, label: itemType.sectionName)
}

// MARK: - Top Sites
Expand Down
6 changes: 5 additions & 1 deletion firefox-ios/Client/Utils/Throttler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import Foundation
import Common

protocol ThrottleProtocol {
func throttle(completion: @escaping () -> Void)
}

/// For any work that needs to be delayed, you can wrap it inside a throttler
/// and specify the delay time, in seconds, and queue.
class Throttler {
class Throttler: ThrottleProtocol {
private let defaultDelay = 0.35

private let threshold: Double
Expand Down
25 changes: 11 additions & 14 deletions firefox-ios/Client/metrics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2349,23 +2349,20 @@ homepage:
tags:
- Homepage

item_viewed:
type: event
section_viewed:
type: labeled_counter
description: |
Records when an item has been viewed on the homepage. See `homepage.viewed` for more details on what is considered a homepage view. This event refers to an item has been scrolled to or seen on an homepage that has been viewed.
extra_keys:
section:
type: string
description: |
The section that the item belongs to on the homepage. This section name is found in `HomepageTelemetry.ItemType` under `sectionName`.
type:
type: string
description: |
The type of item that was tapped on the homepage. This name is found in `HomepageTelemetry.ItemType`.
Records when a section has been viewed on the homepage. See `homepage.viewed` for more details on what is considered a homepage view. This event refers to a section that has been scrolled to or seen on an homepage that has been viewed. The labels matches the values in `HomepageTelemetry.ItemType` under `sectionName`
labels:
- top_sites
- jump_back_in
- bookmarks
- stories
- customize_homepage
bugs:
- https://github.com/mozilla-mobile/firefox-ios/issues/25083
- https://github.com/mozilla-mobile/firefox-ios/issues/26216
data_reviews:
- https://github.com/mozilla-mobile/firefox-ios/pull/26144
- https://github.com/mozilla-mobile/firefox-ios/pull/26234
notification_emails:
- [email protected]
expires: "2025-07-01"
Expand Down
Loading