From 374492a139ec6e4d46cc07c4f631f9bee263f8da Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 30 Oct 2024 17:34:08 +0000 Subject: [PATCH] adding impression pixels for duckplayer in landscape mode (#3493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1201141132935289/1208637202774549/f Tech Design URL: CC: **Description**: the DuckPlayer frontend will start sending a generic 'telemetry' events soon - this PR adds support for the first one, but in a format that can be expanded later without needing new handlers. --- Core/PixelEvent.swift | 4 ++ DuckDuckGo/DuckPlayer/DuckPlayer.swift | 64 ++++++++++++++++++- .../DuckPlayer/YoutubePlayerUserScript.swift | 3 + DuckDuckGoTests/DuckPlayerMocks.swift | 5 ++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 0a021242ea..ad4d2a7a02 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -793,6 +793,7 @@ extension Pixel { case duckPlayerViewFromSERP case duckPlayerViewFromOther case duckPlayerOverlayYoutubeImpressions + case duckPlayerLandscapeLayoutImpressions case duckPlayerOverlayYoutubeWatchHere case duckPlayerSettingAlwaysDuckPlayer case duckPlayerSettingAlwaysSettings @@ -1664,6 +1665,9 @@ extension Pixel.Event { // MARK: - WebView Error Page shown case .webViewErrorPageShown: return "m_errorpageshown" + + // MARK: - DuckPlayer FE Application Telemetry + case .duckPlayerLandscapeLayoutImpressions: return "duckplayer_landscape_layout_impressions" } } } diff --git a/DuckDuckGo/DuckPlayer/DuckPlayer.swift b/DuckDuckGo/DuckPlayer/DuckPlayer.swift index 55e9d907a5..b511962c39 100644 --- a/DuckDuckGo/DuckPlayer/DuckPlayer.swift +++ b/DuckDuckGo/DuckPlayer/DuckPlayer.swift @@ -92,6 +92,50 @@ public enum DuckPlayerReferrer { } } +// Wrapper to allow sibling properties on each event in the future. +struct TelemetryEvent: Decodable { + let attributes: Attributes +} + +// This is the first example of a new telemetry event +struct ImpressionAttributes: Decodable { + enum Layout: String, Decodable { + case landscape = "landscape-layout" + } + + let name: String + let value: Layout +} + +// Designed to represent the discriminated union used by the FE (where all events are schema-driven) +enum Attributes: Decodable { + + // more events can be added here later, without needing a new handler + case impression(ImpressionAttributes) + + private enum CodingKeys: String, CodingKey { + case name + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + + switch name { + case "impression": + let attributes = try ImpressionAttributes(from: decoder) + self = .impression(attributes) + + default: + throw DecodingError.dataCorruptedError( + forKey: .name, + in: container, + debugDescription: "Unknown name value: \(name)" + ) + } + } +} + protocol DuckPlayerProtocol: AnyObject { var settings: DuckPlayerSettings { get } @@ -104,7 +148,8 @@ protocol DuckPlayerProtocol: AnyObject { func openVideoInDuckPlayer(url: URL, webView: WKWebView) func openDuckPlayerSettings(params: Any, message: WKScriptMessage) async -> Encodable? func openDuckPlayerInfo(params: Any, message: WKScriptMessage) async -> Encodable? - + func telemetryEvent(params: Any, message: WKScriptMessage) async -> Encodable? + func initialSetupPlayer(params: Any, message: WKScriptMessage) async -> Encodable? func initialSetupOverlay(params: Any, message: WKScriptMessage) async -> Encodable? @@ -239,6 +284,23 @@ final class DuckPlayer: DuckPlayerProtocol { return nil } + @MainActor + public func telemetryEvent(params: Any, message: WKScriptMessage) async -> Encodable? { + guard let event: TelemetryEvent = DecodableHelper.decode(from: params) else { + return nil + } + + switch event.attributes { + case .impression(let attrs): + switch attrs.value { + case .landscape: + Pixel.fire(pixel: .duckPlayerLandscapeLayoutImpressions) + } + } + + return nil + } + private func encodeUserValues() -> UserValues { return UserValues( duckPlayerMode: featureFlagger.isFeatureOn(.duckPlayer) ? settings.mode : .disabled, diff --git a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift index e85f0ee19b..5e83db541c 100644 --- a/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift +++ b/DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift @@ -37,6 +37,7 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { static let initialSetup = "initialSetup" static let openSettings = "openSettings" static let openInfo = "openInfo" + static let telemetryEvent = "telemetryEvent" } init(duckPlayer: DuckPlayerProtocol) { @@ -79,6 +80,8 @@ final class YoutubePlayerUserScript: NSObject, Subfeature { return duckPlayer.openDuckPlayerSettings case Handlers.openInfo: return duckPlayer.openDuckPlayerInfo + case Handlers.telemetryEvent: + return duckPlayer.telemetryEvent default: assertionFailure("YoutubePlayerUserScript: Failed to parse User Script message: \(methodName)") return nil diff --git a/DuckDuckGoTests/DuckPlayerMocks.swift b/DuckDuckGoTests/DuckPlayerMocks.swift index cf387e5578..c0d8b87f3a 100644 --- a/DuckDuckGoTests/DuckPlayerMocks.swift +++ b/DuckDuckGoTests/DuckPlayerMocks.swift @@ -118,6 +118,11 @@ final class MockDuckPlayerSettings: DuckPlayerSettings { final class MockDuckPlayer: DuckPlayerProtocol { + func telemetryEvent(params: Any, message: WKScriptMessage) async -> (any Encodable)? { + nil + } + + var hostView: UIViewController? func openDuckPlayerSettings(params: Any, message: WKScriptMessage) async -> (any Encodable)? {