Skip to content

Commit

Permalink
Add pixels to track Duck Player usage (#2760)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1177771139624306/1207208910013350/f

Description:
This change updates existing Duck Player pixels and adds a bunch of new pixels.
  • Loading branch information
ayoy authored May 10, 2024
1 parent d094931 commit 89d4dda
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 37 deletions.
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 {
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 firing the pixel here.
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

0 comments on commit 89d4dda

Please sign in to comment.