diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift index f64a61f558..609b1d3af8 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerNavigationHandler.swift @@ -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 @@ -575,8 +575,6 @@ final class DuckPlayerNavigationHandler: NSObject { // Watch in YT videos always open in new tab redirectToYouTubeVideo(url: url, webView: webView, forceNewTab: true) } - - } } @@ -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) @@ -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, @@ -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) @@ -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 } @@ -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 @@ -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. @@ -890,7 +904,7 @@ extension DuckPlayerNavigationHandler: DuckPlayerNavigationHandling { guard isDuckPlayerFeatureEnabled else { return false } - + // Only account for in 'Always' mode if duckPlayerMode == .disabled { return false diff --git a/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift b/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift index 459b762f40..6f3339dd72 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayerOverlayUsagePixels.swift @@ -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 @@ -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..