Skip to content

Commit

Permalink
[DuckPlayer] Overlay Usage Pixels (#3565)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1204099484721401/1208739766555300/f

Description:

Unifies navigation tracking and pixel firing in a single method
Adds temporary Overlay usage pixels
  • Loading branch information
afterxleep authored Nov 13, 2024
1 parent b645554 commit 1d14059
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 247 deletions.
32 changes: 23 additions & 9 deletions DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ final class DuckPlayerNavigationHandler: NSObject {
pixelFiring: PixelFiring.Type = Pixel.self,
dailyPixelFiring: DailyPixelFiring.Type = DailyPixel.self,
tabNavigationHandler: DuckPlayerTabNavigationHandling? = nil,
duckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring? = nil) {
duckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring? = DuckPlayerOverlayUsagePixels()) {
self.duckPlayer = duckPlayer
self.featureFlagger = featureFlagger
self.appSettings = appSettings
Expand Down Expand Up @@ -575,8 +575,6 @@ final class DuckPlayerNavigationHandler: NSObject {
// Watch in YT videos always open in new tab
redirectToYouTubeVideo(url: url, webView: webView, forceNewTab: true)
}


}

}
Expand Down Expand Up @@ -638,7 +636,9 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
// Before performing the simulated request
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
self.performRequest(request: newRequest, webView: webView)
self.duckPlayerOverlayUsagePixels?.handleNavigationAndFirePixels(url: url, duckPlayerMode: self.duckPlayerMode)
self.fireDuckPlayerPixels(webView: webView)

}
} else {
redirectToYouTubeVideo(url: url, webView: webView)
Expand All @@ -663,9 +663,6 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
@MainActor
func handleURLChange(webView: WKWebView) -> DuckPlayerNavigationHandlerURLChangeResult {

// Track overlayUsagePixels
duckPlayerOverlayUsagePixels?.registerNavigation(url: webView.url)

// We want to prevent multiple simultaneous redirects
// This can be caused by Duplicate Nav events, and quick URL changes
if let lastTimestamp = lastURLChangeHandling,
Expand All @@ -683,6 +680,12 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
return .notHandled(.duplicateNavigation)
}

// Overlay Usage Pixel handling
if let url = webView.url {
duckPlayerOverlayUsagePixels?.handleNavigationAndFirePixels(url: url, duckPlayerMode: duckPlayerMode)
lastURLChangeHandling = Date()
}

// Check if DuckPlayer feature is enabled
guard isDuckPlayerFeatureEnabled else {
return .notHandled(.featureOff)
Expand Down Expand Up @@ -735,7 +738,7 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
@MainActor
func handleGoBack(webView: WKWebView) {

guard isDuckPlayerFeatureEnabled else {
guard let url = webView.url, url.isDuckPlayer, isDuckPlayerFeatureEnabled else {
webView.goBack()
return
}
Expand Down Expand Up @@ -783,7 +786,7 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
guard let url = webView.url else {
return
}

if url.isDuckPlayer, duckPlayerMode != .disabled {
redirectToDuckPlayerVideo(url: url, webView: webView, disableNewTab: true)
return
Expand Down Expand Up @@ -833,6 +836,17 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
// Reset allowFirstVideo
duckPlayer.settings.allowFirstVideo = false

// Overlay Usage Pixel handling for Direct Navigation
if let url = webView.url, !url.isYoutube {
duckPlayerOverlayUsagePixels?.handleNavigationAndFirePixels(url: url, duckPlayerMode: duckPlayerMode)
}
// Reset Overlay Last Fired pixel after the page is loaded
// A delay is required as Youtube sometimes performs an extra redirect on load
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.duckPlayerOverlayUsagePixels?.lastFiredPixel = nil
}


}

/// Resets settings when the web view starts loading a new page.
Expand Down Expand Up @@ -890,7 +904,7 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling {
guard isDuckPlayerFeatureEnabled else {
return false
}

// Only account for in 'Always' mode
if duckPlayerMode == .disabled {
return false
Expand Down
116 changes: 50 additions & 66 deletions DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,16 @@ protocol DuckPlayerOverlayPixelFiring {

var pixelFiring: PixelFiring.Type { get set }
var navigationHistory: [URL] { get set }
var lastFiredPixel: Pixel.Event? { get set }

func registerNavigation(url: URL?)
func navigationBack(duckPlayerMode: DuckPlayerMode)
func navigationReload(duckPlayerMode: DuckPlayerMode)
func navigationWithinYoutube(duckPlayerMode: DuckPlayerMode)
func navigationOutsideYoutube(duckPlayerMode: DuckPlayerMode)
func navigationClosed(duckPlayerMode: DuckPlayerMode)
func overlayIdle(duckPlayerMode: DuckPlayerMode)

func handleNavigationAndFirePixels(url: URL?, duckPlayerMode: DuckPlayerMode)
}

final class DuckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring {

var pixelFiring: PixelFiring.Type
var navigationHistory: [URL] = []
var lastFiredPixel: Pixel.Event?

private var idleTimer: Timer?
private var idleTimeInterval: TimeInterval
Expand All @@ -49,75 +44,64 @@ final class DuckPlayerOverlayUsagePixels: DuckPlayerOverlayPixelFiring {
self.idleTimeInterval = timeoutInterval
}

// Method to reset the idle timer
private func resetIdleTimer() {
idleTimer?.invalidate()
idleTimer = nil
}

func registerNavigation(url: URL?) {
func handleNavigationAndFirePixels(url: URL?, duckPlayerMode: DuckPlayerMode) {
guard let url = url else { return }
navigationHistory.append(url)

// Cancel and reset the idle timer whenever a new navigation occurs
resetIdleTimer()
}

func navigationBack(duckPlayerMode: DuckPlayerMode) {
guard duckPlayerMode == .alwaysAsk,
let lastURL = navigationHistory.last,
lastURL.isYoutubeWatch else { return }

pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationBack, withAdditionalParameters: [:])
}

func navigationReload(duckPlayerMode: DuckPlayerMode) {
guard duckPlayerMode == .alwaysAsk,
let lastURL = navigationHistory.last,
lastURL.isYoutubeWatch else { return }
let comparisonURL = url.forComparison()

pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationRefresh, withAdditionalParameters: [:])
}
// Only append the URL if it's different from the last entry in normalized form
navigationHistory.append(comparisonURL)

func navigationWithinYoutube(duckPlayerMode: DuckPlayerMode) {
// DuckPlayer is in Ask Mode, there's navigation history, and last URL is a YouTube Watch Video
guard duckPlayerMode == .alwaysAsk,
navigationHistory.count > 1,
let currentURL = navigationHistory.last,
let previousURL = navigationHistory.dropLast().last,
previousURL.isYoutubeWatch,
currentURL.isYoutube else { return }

pixelFiring.fire(.duckPlayerYouTubeNavigationWithinYouTube, withAdditionalParameters: [:])
}
previousURL.isYoutubeWatch else { return }

var isReload = false
// Check for a reload condition: when current videoID is the same as Previous
if let currentVideoID = currentURL.youtubeVideoParams?.videoID,
let previousVideoID = previousURL.youtubeVideoParams?.videoID,
!previousURL.isDuckPlayer, !currentURL.isDuckPlayer {
isReload = currentVideoID == previousVideoID
}

func navigationOutsideYoutube(duckPlayerMode: DuckPlayerMode) {
guard duckPlayerMode == .alwaysAsk,
navigationHistory.count > 1,
let currentURL = navigationHistory.last,
let previousURL = navigationHistory.dropLast().last,
previousURL.isYoutubeWatch,
!currentURL.isYoutube else { return }
// Fire the reload pixel if this is a reload navigation
if isReload {
firePixel(.duckPlayerYouTubeOverlayNavigationRefresh)
} else {
// Determine if it’s a back navigation by looking further back in history
let isBackNavigation = navigationHistory.count > 2 &&
navigationHistory[navigationHistory.count - 3].forComparison() == currentURL.forComparison()

// Fire the appropriate pixel based on navigation type
if isBackNavigation {
firePixel(.duckPlayerYouTubeOverlayNavigationBack)
} else if previousURL.isYoutubeWatch && currentURL.isYoutube {
// Forward navigation within YouTube (including non-video URLs)
firePixel(.duckPlayerYouTubeNavigationWithinYouTube)
} else if previousURL.isYoutubeWatch && !currentURL.isYoutube && !currentURL.isDuckPlayer {
// Navigation outside YouTube
firePixel(.duckPlayerYouTubeOverlayNavigationOutsideYoutube)
navigationHistory.removeAll()
}
}

pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationOutsideYoutube, withAdditionalParameters: [:])
// Truncation logic: Remove all URLs up to the last occurrence of the current URL in normalized form
if navigationHistory.count > 0 {
if let lastOccurrenceIndex = (0..<navigationHistory.count - 1).last(where: { navigationHistory[$0].forComparison() == comparisonURL }) {
navigationHistory = Array(navigationHistory.prefix(upTo: lastOccurrenceIndex + 1))
}
}
}

func navigationClosed(duckPlayerMode: DuckPlayerMode) {

guard duckPlayerMode == .alwaysAsk,
let lastURL = navigationHistory.last,
lastURL.isYoutubeWatch else { return }

pixelFiring.fire(.duckPlayerYouTubeOverlayNavigationClosed, withAdditionalParameters: [:])


private func firePixel(_ pixel: Pixel.Event) {
if lastFiredPixel == .duckPlayerYouTubeOverlayNavigationRefresh && pixel == .duckPlayerYouTubeOverlayNavigationRefresh {
return
}
lastFiredPixel = pixel
pixelFiring.fire(pixel, withAdditionalParameters: [:])
}

func overlayIdle(duckPlayerMode: DuckPlayerMode) {
guard duckPlayerMode == .alwaysAsk,
let lastURL = navigationHistory.last,
lastURL.isYoutubeWatch else { return }

idleTimer = Timer.scheduledTimer(withTimeInterval: idleTimeInterval, repeats: false) { [weak self] _ in
self?.pixelFiring.fire(.duckPlayerYouTubeNavigationIdle30, withAdditionalParameters: [:])
}
}
}
Loading

0 comments on commit 1d14059

Please sign in to comment.