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

Add pixels to track Duck Player usage #2760

Merged
merged 9 commits into from
May 10, 2024
3 changes: 2 additions & 1 deletion DuckDuckGo/Preferences/Model/DuckPlayerPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
// limitations under the License.
//

import Foundation
import Combine
import Foundation
import PixelKit
ayoy marked this conversation as resolved.
Show resolved Hide resolved

protocol DuckPlayerPreferencesPersistor {
/// The persistor hadles raw Bool values but each one translates into a DuckPlayerMode:
Expand Down
9 changes: 9 additions & 0 deletions DuckDuckGo/Preferences/View/PreferencesDuckPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import PreferencesViews
import PixelKit
import SwiftUI
import SwiftUIExtensions

Expand All @@ -30,6 +31,14 @@ extension Preferences {
model.duckPlayerMode
} set: { newValue in
model.duckPlayerMode = newValue
switch model.duckPlayerMode {
ayoy marked this conversation as resolved.
Show resolved Hide resolved
case .enabled:
PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysSettings)
case .alwaysAsk:
PixelKit.fire(GeneralPixel.duckPlayerSettingBackToDefault)
case .disabled:
PixelKit.fire(GeneralPixel.duckPlayerSettingNeverSettings)
}
}
}

Expand Down
36 changes: 30 additions & 6 deletions DuckDuckGo/Statistics/GeneralPixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,17 @@ enum GeneralPixel: PixelKitEventV2 {
case duckPlayerViewFromYoutubeAutomatic
case duckPlayerViewFromSERP
case duckPlayerViewFromOther
case duckPlayerSettingAlways
case duckPlayerSettingNever
case duckPlayerOverlayYoutubeImpressions
case duckPlayerOverlayYoutubeWatchHere
case duckPlayerSettingAlwaysDuckPlayer
case duckPlayerSettingAlwaysOverlaySERP
case duckPlayerSettingAlwaysOverlayYoutube
case duckPlayerSettingAlwaysSettings
case duckPlayerSettingNeverOverlaySERP
case duckPlayerSettingNeverOverlayYoutube
case duckPlayerSettingNeverSettings
case duckPlayerSettingBackToDefault
case duckPlayerWatchOnYoutube

// Dashboard
case dashboardProtectionAllowlistAdd(triggerOrigin: String?)
Expand Down Expand Up @@ -413,12 +421,28 @@ enum GeneralPixel: PixelKitEventV2 {
return "m_mac_duck-player_view-from_serp"
case .duckPlayerViewFromOther:
return "m_mac_duck-player_view-from_other"
case .duckPlayerSettingAlways:
return "m_mac_duck-player_setting_always"
case .duckPlayerSettingNever:
return "m_mac_duck-player_setting_never"
case .duckPlayerSettingAlwaysSettings:
return "m_mac_duck-player_setting_always_settings"
case .duckPlayerOverlayYoutubeImpressions:
return "m_mac_duck-player_overlay_youtube_impressions"
case .duckPlayerOverlayYoutubeWatchHere:
return "m_mac_duck-player_overlay_youtube_watch_here"
case .duckPlayerSettingAlwaysDuckPlayer:
return "m_mac_duck-player_setting_always_duck-player"
case .duckPlayerSettingAlwaysOverlaySERP:
return "m_mac_duck-player_setting_always_overlay_serp"
case .duckPlayerSettingAlwaysOverlayYoutube:
return "m_mac_duck-player_setting_always_overlay_youtube"
case .duckPlayerSettingNeverOverlaySERP:
return "m_mac_duck-player_setting_never_overlay_serp"
case .duckPlayerSettingNeverOverlayYoutube:
return "m_mac_duck-player_setting_never_overlay_youtube"
case .duckPlayerSettingNeverSettings:
return "m_mac_duck-player_setting_never_settings"
case .duckPlayerSettingBackToDefault:
return "m_mac_duck-player_setting_back-to-default"
case .duckPlayerWatchOnYoutube:
return "m_mac_duck-player_watch_on_youtube"

case .dashboardProtectionAllowlistAdd:
return "m_mac_mp_wla"
Expand Down
4 changes: 3 additions & 1 deletion DuckDuckGo/Tab/TabExtensions/DuckPlayerTabExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ extension DuckPlayerTabExtension: NavigationResponder {
// when currently displayed content is the Duck Player and loading a YouTube URL, don‘t override it
if navigationAction.targetFrame?.url.isDuckPlayer == true,
navigationAction.targetFrame?.url.youtubeVideoID == videoID {
PixelKit.fire(GeneralPixel.duckPlayerWatchOnYoutube)
return .next

// If this is a child tab of a Duck Player and it's loading a YouTube URL, don‘t override it
Expand Down Expand Up @@ -312,7 +313,8 @@ extension DuckPlayerTabExtension: NavigationResponder {
return
}
if navigation.url.isDuckPlayer {
PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .legacyDaily)
let setting = duckPlayer.mode == .enabled ? "always" : "default"
PixelKit.fire(GeneralPixel.duckPlayerDailyUniqueView, frequency: .legacyDaily, withAdditionalParameters: ["setting": setting])
}
}

Expand Down
69 changes: 51 additions & 18 deletions DuckDuckGo/YoutubePlayer/DuckPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,59 @@ final class DuckPlayer {

// MARK: - Common Message Handlers

public func handleSetUserValues(params: Any, message: UserScriptMessage) -> Encodable? {
guard let userValues: UserValues = DecodableHelper.decode(from: params) else {
assertionFailure("YoutubeOverlayUserScript: expected JSON representation of UserValues")
return nil
}
// swiftlint:disable:next cyclomatic_complexity
public func handleSetUserValuesMessage(
from origin: YoutubeOverlayUserScript.MessageOrigin
) -> (_ params: Any, _ message: UserScriptMessage) -> Encodable? {

return { [weak self] params, _ -> Encodable? in
guard let self else {
return nil
}
guard let userValues: UserValues = DecodableHelper.decode(from: params) else {
assertionFailure("YoutubeOverlayUserScript: expected JSON representation of UserValues")
return nil
}

self.preferences.youtubeOverlayInteracted = userValues.overlayInteracted
self.preferences.duckPlayerMode = userValues.duckPlayerMode
let modeDidChange = self.preferences.duckPlayerMode != userValues.duckPlayerMode
let overlayDidInteract = !self.preferences.youtubeOverlayInteracted && userValues.overlayInteracted

if modeDidChange {
self.preferences.duckPlayerMode = userValues.duckPlayerMode
if case .enabled = userValues.duckPlayerMode {
switch origin {
case .duckPlayer:
PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysDuckPlayer)
case .serpOverlay:
PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysOverlaySERP)
case .youtubeOverlay:
PixelKit.fire(GeneralPixel.duckPlayerSettingAlwaysOverlayYoutube)
}
}
}

return encodeUserValues()
if overlayDidInteract {
self.preferences.youtubeOverlayInteracted = userValues.overlayInteracted

// If user checks "Remember my choice" and clicks "Watch here", we won't show
// the overlay anymore, but will keep presenting Dax logos (the mode stays at
// "alwaysAsk" which may be a bit counterintuitive, but it's the overlayInteracted
// flag that plays a role here. We want to track users opting in to not showing overlays,
// hence fiting the pixel here.
ayoy marked this conversation as resolved.
Show resolved Hide resolved
if userValues.duckPlayerMode == .alwaysAsk {
switch origin {
case .serpOverlay:
PixelKit.fire(GeneralPixel.duckPlayerSettingNeverOverlaySERP)
case .youtubeOverlay:
PixelKit.fire(GeneralPixel.duckPlayerSettingNeverOverlayYoutube)
default:
break
}
}
}

return self.encodeUserValues()
}
}

public func handleGetUserValues(params: Any, message: UserScriptMessage) -> Encodable? {
Expand Down Expand Up @@ -150,16 +193,6 @@ final class DuckPlayer {
modeCancellable = preferences.$duckPlayerMode
.removeDuplicates()
.dropFirst(1)
.handleEvents(receiveOutput: { mode in
switch mode {
case .enabled:
PixelKit.fire(GeneralPixel.duckPlayerSettingAlways)
case .alwaysAsk:
PixelKit.fire(GeneralPixel.duckPlayerSettingBackToDefault)
case .disabled:
PixelKit.fire(GeneralPixel.duckPlayerSettingNever)
}
})
.prepend(preferences.duckPlayerMode)
.assign(to: \.mode, onWeaklyHeld: self)
} else {
Expand Down
45 changes: 34 additions & 11 deletions DuckDuckGo/YoutubePlayer/YoutubeOverlayUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ protocol YoutubeOverlayUserScriptDelegate: AnyObject {

final class YoutubeOverlayUserScript: NSObject, Subfeature {

enum MessageOrigin {
case duckPlayer, serpOverlay, youtubeOverlay

init?(url: URL) {
switch url.host {
case "duckduckgo.com":
self = .serpOverlay
case "www.youtube.com":
self = .youtubeOverlay
default:
return nil
}
}
}

let duckPlayerPreferences: DuckPlayerPreferences
weak var broker: UserScriptMessageBroker?
weak var delegate: YoutubeOverlayUserScriptDelegate?
Expand Down Expand Up @@ -60,7 +75,11 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature {
func handler(forMethodNamed methodName: String) -> Subfeature.Handler? {
switch MessageNames(rawValue: methodName) {
case .setUserValues:
return DuckPlayer.shared.handleSetUserValues
guard let url = webView?.url, let origin = MessageOrigin(url: url) else {
assertionFailure("YoutubeOverlayUserScript: Unexpected message origin: \(String(describing: webView?.url))")
return nil
}
return DuckPlayer.shared.handleSetUserValuesMessage(from: origin)
case .getUserValues:
return DuckPlayer.shared.handleGetUserValues
case .openDuckPlayer:
Expand Down Expand Up @@ -111,20 +130,24 @@ extension YoutubeOverlayUserScript {
return nil
}
let pixelName = parameters["pixelName"] as? String
if pixelName == "play.use" || pixelName == "play.do_not_use" {

switch pixelName {
case "play.use":
duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true
if pixelName == "play.use" {
PixelKit.fire(GeneralPixel.duckPlayerViewFromYoutubeViaMainOverlay)
PixelKit.fire(GeneralPixel.duckPlayerViewFromYoutubeViaMainOverlay)
// Temporary pixel for first time user uses Duck Player
if AppDelegate.isNewUser {
PixelKit.fire(GeneralPixel.watchInDuckPlayerInitial, frequency: .legacyInitial)
}
case "play.do_not_use":
duckPlayerPreferences.youtubeOverlayAnyButtonPressed = true
PixelKit.fire(GeneralPixel.duckPlayerOverlayYoutubeWatchHere)
case "overlay":
PixelKit.fire(GeneralPixel.duckPlayerOverlayYoutubeImpressions)
default:
break
}

// Temporary pixel for first time user uses Duck Player
if !AppDelegate.isNewUser {
return nil
}
if pixelName == "play.use" {
PixelKit.fire(GeneralPixel.watchInDuckPlayerInitial, frequency: .legacyInitial)
}
return nil
}
}
2 changes: 1 addition & 1 deletion DuckDuckGo/YoutubePlayer/YoutubePlayerUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature {
case .getUserValues:
return DuckPlayer.shared.handleGetUserValues
case .setUserValues:
return DuckPlayer.shared.handleSetUserValues
return DuckPlayer.shared.handleSetUserValuesMessage(from: .duckPlayer)
default:
assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)")
return nil
Expand Down
Loading