Skip to content
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

[DuckPlayer] Overlay Usage Pixels #3565

Merged
merged 4 commits into from
Nov 13, 2024
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
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
Loading