Skip to content

Commit

Permalink
Add Web UI loading state pixels (#2531)
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira authored Apr 3, 2024
1 parent ebb9320 commit 7124c49
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 2 deletions.
5 changes: 4 additions & 1 deletion DuckDuckGo/DBP/DBPHomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.weeklyReportScanning,
.weeklyReportRemovals,
.scanningEventNewMatch,
.scanningEventReAppearance:
.scanningEventReAppearance,
.webUILoadingFailed,
.webUILoadingStarted,
.webUILoadingSuccess:
Pixel.fire(.pixelKitEvent(event))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public enum DataBrokerProtectionPixels {
static let hadReAppereance = "had_re-appearance"
static let scanCoverage = "scan_coverage"
static let removals = "removals"
static let environmentKey = "environment"
}

case error(error: DataBrokerProtectionError, dataBroker: String)
Expand Down Expand Up @@ -127,6 +128,11 @@ public enum DataBrokerProtectionPixels {
case weeklyReportRemovals(removals: Int)
case scanningEventNewMatch
case scanningEventReAppearance

// Web UI - loading errors
case webUILoadingStarted(environment: String)
case webUILoadingFailed(errorCategory: String)
case webUILoadingSuccess(environment: String)
}

extension DataBrokerProtectionPixels: PixelKitEvent {
Expand Down Expand Up @@ -205,6 +211,10 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
case .weeklyReportRemovals: return "m_mac_dbp_event_weekly-report_removals"
case .scanningEventNewMatch: return "m_mac_dbp_event_scanning-events_new-match"
case .scanningEventReAppearance: return "m_mac_dbp_event_scanning-events_re-appearance"

case .webUILoadingStarted: return "m_mac_dbp_web_ui_loading_started"
case .webUILoadingSuccess: return "m_mac_dbp_web_ui_loading_success"
case .webUILoadingFailed: return "m_mac_dbp_web_ui_loading_failed"
}
}

Expand Down Expand Up @@ -270,6 +280,12 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
return [Consts.hadNewMatch: hadNewMatch ? "1" : "0", Consts.hadReAppereance: hadReAppereance ? "1" : "0", Consts.scanCoverage: scanCoverage.description]
case .weeklyReportRemovals(let removals):
return [Consts.removals: String(removals)]
case .webUILoadingStarted(let environment):
return [Consts.environmentKey: environment]
case .webUILoadingSuccess(let environment):
return [Consts.environmentKey: environment]
case .webUILoadingFailed(let error):
return [Consts.errorCategoryKey: error]
case .backgroundAgentStarted,
.backgroundAgentRunOperationsAndStartSchedulerIfPossible,
.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile,
Expand Down Expand Up @@ -366,7 +382,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.weeklyReportScanning,
.weeklyReportRemovals,
.scanningEventNewMatch,
.scanningEventReAppearance:
.scanningEventReAppearance,
.webUILoadingFailed,
.webUILoadingStarted,
.webUILoadingSuccess:

PixelKit.fire(event)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// DataBrokerProtectionWebUIPixels.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Common
import BrowserServicesKit
import PixelKit

final class DataBrokerProtectionWebUIPixels {

enum PixelType {
case loading
case success
}

let pixelHandler: EventMapping<DataBrokerProtectionPixels>
private var wasHTTPErrorPixelFired = false

init(pixelHandler: EventMapping<DataBrokerProtectionPixels>) {
self.pixelHandler = pixelHandler
}

func firePixel(for error: Error) {
if wasHTTPErrorPixelFired {
wasHTTPErrorPixelFired = false // We reset the flag
return
}

let nsError = error as NSError

if nsError.domain == NSURLErrorDomain {
let statusCode = nsError.code
if statusCode >= 400 && statusCode < 600 {
pixelHandler.fire(.webUILoadingFailed(errorCategory: "httpError-\(statusCode)"))
wasHTTPErrorPixelFired = true
} else {
pixelHandler.fire(.webUILoadingFailed(errorCategory: "other-\(nsError.code)"))
}
} else {
pixelHandler.fire(.webUILoadingFailed(errorCategory: "other-\(nsError.code)"))
}
}

func firePixel(for selectedURL: DataBrokerProtectionWebUIURLType, type: PixelType) {
let environment = selectedURL == .custom ? "staging" : "production"

switch type {
case .loading:
pixelHandler.fire(.webUILoadingStarted(environment: environment))
case .success:
pixelHandler.fire(.webUILoadingSuccess(environment: environment))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import Cocoa
import SwiftUI
import Common
import BrowserServicesKit
import PixelKit
import WebKit
import Combine

Expand All @@ -29,6 +31,8 @@ final public class DataBrokerProtectionViewController: NSViewController {
private var loader: NSProgressIndicator!
private let webUISettings: DataBrokerProtectionWebUIURLSettingsRepresentable
private let webUIViewModel: DBPUIViewModel
private let pixelHandler: EventMapping<DataBrokerProtectionPixels>
private let webUIPixel: DataBrokerProtectionWebUIPixels

private let openURLHandler: (URL?) -> Void
private var reloadObserver: NSObjectProtocol?
Expand All @@ -43,6 +47,8 @@ final public class DataBrokerProtectionViewController: NSViewController {
self.dataManager = dataManager
self.openURLHandler = openURLHandler
self.webUISettings = webUISettings
self.pixelHandler = DataBrokerProtectionPixelsHandler()
self.webUIPixel = DataBrokerProtectionWebUIPixels(pixelHandler: pixelHandler)
self.webUIViewModel = DBPUIViewModel(dataManager: dataManager,
scheduler: scheduler,
webUISettings: webUISettings,
Expand Down Expand Up @@ -82,6 +88,7 @@ final public class DataBrokerProtectionViewController: NSViewController {
addLoadingIndicator()

if let url = URL(string: webUISettings.selectedURL) {
webUIPixel.firePixel(for: webUISettings.selectedURLType, type: .loading)
webView?.load(url)
} else {
removeLoadingIndicator()
Expand Down Expand Up @@ -126,11 +133,40 @@ extension DataBrokerProtectionViewController: WKUIDelegate {

extension DataBrokerProtectionViewController: WKNavigationDelegate {

public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
fireWebViewError(error)
}

public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: any Error) {
fireWebViewError(error)
}

private func fireWebViewError(_ error: Error) {
webUIPixel.firePixel(for: error)
removeLoadingIndicator()
}

public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
guard let statusCode = (navigationResponse.response as? HTTPURLResponse)?.statusCode else {
// if there's no http status code to act on, exit and allow navigation
return .allow
}

if statusCode >= 400 {
webUIPixel.firePixel(for: NSError(domain: NSURLErrorDomain, code: statusCode))
return .cancel
}

return .allow
}

public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
loader.startAnimation(nil)
}

public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
removeLoadingIndicator()

webUIPixel.firePixel(for: webUISettings.selectedURLType, type: .success)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// DataBrokerProtectionWebUIPixelsTests.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import XCTest
import Foundation
@testable import DataBrokerProtection

final class DataBrokerProtectionWebUIPixelsTests: XCTestCase {

let handler = MockDataBrokerProtectionPixelsHandler()

override func tearDown() {
handler.clear()
}

func testWhenURLErrorIsHttp_thenCorrectPixelIsFired() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404))

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.params!["error_category"],
"httpError-404"
)
}

func testWhenURLErrorIsNotHttp_thenCorrectPixelIsFired() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 100))

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.params!["error_category"],
"other-100"
)
}

func testWhenErrorIsNotURL_thenCorrectPixelIsFired() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500))

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.params!["error_category"],
"other-500"
)
}

func testWhenSelectedURLisCustomAndLoading_thenStagingParamIsSent() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: .custom, type: .loading)

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.name,
DataBrokerProtectionPixels.webUILoadingStarted(environment: "staging").name
)
XCTAssertEqual(
lastPixelFired.params!["environment"],
"staging"
)
}

func testWhenSelectedURLisProductionAndLoading_thenProductionParamIsSent() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: .production, type: .loading)

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.name,
DataBrokerProtectionPixels.webUILoadingStarted(environment: "staging").name
)
XCTAssertEqual(
lastPixelFired.params!["environment"],
"production"
)
}

func testWhenSelectedURLisCustomAndSuccess_thenStagingParamIsSent() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: .custom, type: .success)

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.name,
DataBrokerProtectionPixels.webUILoadingSuccess(environment: "staging").name
)
XCTAssertEqual(
lastPixelFired.params!["environment"],
"staging"
)
}

func testWhenSelectedURLisProductionAndSuccess_thenProductionParamIsSent() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: .production, type: .success)

let lastPixelFired = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
lastPixelFired.name,
DataBrokerProtectionPixels.webUILoadingSuccess(environment: "staging").name
)
XCTAssertEqual(
lastPixelFired.params!["environment"],
"production"
)
}

func testWhenHTTPPixelIsFired_weDoNotFireAnotherPixelRightAway() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404))
sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500))

let httpPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
httpPixel.params!["error_category"],
"httpError-404"
)
XCTAssertEqual(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count, 1) // We only fire one pixel
}

func testWhenHTTPPixelIsFired_weFireTheNextErrorPixelOnTheSecondTry() {
let sut = DataBrokerProtectionWebUIPixels(pixelHandler: handler)

sut.firePixel(for: NSError(domain: NSURLErrorDomain, code: 404))
sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500))
sut.firePixel(for: NSError(domain: NSCocoaErrorDomain, code: 500))

let httpPixel = MockDataBrokerProtectionPixelsHandler.lastPixelsFired.first!

XCTAssertEqual(
httpPixel.params!["error_category"],
"httpError-404"
)
XCTAssertEqual(MockDataBrokerProtectionPixelsHandler.lastPixelsFired.count, 2) // We fire the HTTP pixel and the second cocoa error pixel
}
}

0 comments on commit 7124c49

Please sign in to comment.