Skip to content

Commit

Permalink
Add iOS NetP pixels (#2261)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1206057769488160/f

Description:

Add iOS NetP pixels for active/new user, VPN enabled success rate, tunnel failure/recovery, and latency.

Steps to test this PR:

Confirm pixel submission on Kibana
Make sure active user pixel includes the cohort value
  • Loading branch information
quanganhdo authored Dec 12, 2023
1 parent ad9ab42 commit 8697128
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 6 deletions.
23 changes: 21 additions & 2 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import BrowserServicesKit
import Bookmarks
import Configuration
import DDGSync
import NetworkProtection

// swiftlint:disable file_length
extension Pixel {
Expand Down Expand Up @@ -316,8 +317,19 @@ extension Pixel {
case networkProtectionActiveUser
case networkProtectionNewUser

case networkProtectionEnableAttemptConnecting
case networkProtectionEnableAttemptSuccess
case networkProtectionEnableAttemptFailure

case networkProtectionTunnelFailureDetected
case networkProtectionTunnelFailureRecovered

case networkProtectionLatency(quality: NetworkProtectionLatencyMonitor.ConnectionQuality)
case networkProtectionLatencyError

case networkProtectionEnabledOnSearch

case networkProtectionRekeyCompleted
case networkProtectionLatency

case networkProtectionTunnelConfigurationNoServerRegistrationInfo
case networkProtectionTunnelConfigurationCouldNotSelectClosestServer
Expand Down Expand Up @@ -851,8 +863,15 @@ extension Pixel.Event {

case .networkProtectionActiveUser: return "m_netp_daily_active_d"
case .networkProtectionNewUser: return "m_netp_daily_active_u"
case .networkProtectionEnableAttemptConnecting: return "m_netp_ev_enable_attempt"
case .networkProtectionEnableAttemptSuccess: return "m_netp_ev_enable_attempt_success"
case .networkProtectionEnableAttemptFailure: return "m_netp_ev_enable_attempt_failure"
case .networkProtectionTunnelFailureDetected: return "m_netp_ev_tunnel_failure"
case .networkProtectionTunnelFailureRecovered: return "m_netp_ev_tunnel_failure_recovered"
case .networkProtectionLatency(let quality): return "m_netp_ev_\(quality.rawValue)_latency"
case .networkProtectionLatencyError: return "m_netp_ev_latency_error_d"
case .networkProtectionRekeyCompleted: return "m_netp_rekey_completed"
case .networkProtectionLatency: return "m_netp_latency"
case .networkProtectionEnabledOnSearch: return "m_netp_enabled_on_search"
case .networkProtectionTunnelConfigurationNoServerRegistrationInfo: return "m_netp_tunnel_config_error_no_server_registration_info"
case .networkProtectionTunnelConfigurationCouldNotSelectClosestServer: return "m_netp_tunnel_config_error_could_not_select_closest_server"
case .networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey: return "m_netp_tunnel_config_error_could_not_get_peer_public_key"
Expand Down
84 changes: 84 additions & 0 deletions Core/UniquePixel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// UniquePixel.swift
// DuckDuckGo
//
// 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

/// A variant of pixel that is fired just once. Ever.
///
/// The 'fire' method mimics standard Pixel API.
/// The 'onComplete' closure is always called - even when no pixel is fired.
/// In those scenarios a 'UniquePixelError' is returned denoting the reason.
///
public final class UniquePixel {

public enum Error: Swift.Error {

case alreadyFired

}

private enum Constant {

static let uniquePixelStorageIdentifier = "com.duckduckgo.unique.pixel.storage"

}

public static let storage = UserDefaults(suiteName: Constant.uniquePixelStorageIdentifier)!

/// Sends a unique Pixel
/// This requires the pixel name to end with `_u`
public static func fire(pixel: Pixel.Event,
withAdditionalParameters params: [String: String] = [:],
onComplete: @escaping (Swift.Error?) -> Void = { _ in }) {
guard pixel.name.hasSuffix("_u") else {
assertionFailure("Unique pixel: must end with _u")
return
}

if !pixel.hasBeenFiredEver(uniquePixelStorage: storage) {
Pixel.fire(pixel: pixel, withAdditionalParameters: params, onComplete: onComplete)
storage.set(Date(), forKey: pixel.name)
} else {
onComplete(Error.alreadyFired)
}
}

public static func dateString(for date: Date?) -> String {
guard let date else { return "" }

let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar.current
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
dateFormatter.dateFormat = "yyyy-MM-dd"

return dateFormatter.string(from: date)
}
}

extension Pixel.Event {

public func lastFireDate(uniquePixelStorage: UserDefaults) -> Date? {
uniquePixelStorage.object(forKey: name) as? Date
}

func hasBeenFiredEver(uniquePixelStorage: UserDefaults) -> Bool {
lastFireDate(uniquePixelStorage: uniquePixelStorage) != nil
}

}
4 changes: 4 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@
B6BA95C528894A28004ABA20 /* BrowsingMenuViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95C428894A28004ABA20 /* BrowsingMenuViewController.storyboard */; };
B6BA95E828924730004ABA20 /* JSAlertController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6BA95E728924730004ABA20 /* JSAlertController.storyboard */; };
B6CB93E5286445AB0090FEB4 /* Base64DownloadSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CB93E4286445AB0090FEB4 /* Base64DownloadSession.swift */; };
BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC234F62B27F51100D3C798 /* UniquePixel.swift */; };
C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */; };
C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */; };
C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */; };
Expand Down Expand Up @@ -2276,6 +2277,7 @@
B6BA95C428894A28004ABA20 /* BrowsingMenuViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BrowsingMenuViewController.storyboard; sourceTree = "<group>"; };
B6BA95E728924730004ABA20 /* JSAlertController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = JSAlertController.storyboard; sourceTree = "<group>"; };
B6CB93E4286445AB0090FEB4 /* Base64DownloadSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64DownloadSession.swift; sourceTree = "<group>"; };
BDC234F62B27F51100D3C798 /* UniquePixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniquePixel.swift; sourceTree = "<group>"; };
C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillViews.swift; sourceTree = "<group>"; };
C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkOrFolderTests.swift; sourceTree = "<group>"; };
C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4592,6 +4594,7 @@
F1134EAE1F40AB2300B73467 /* Parser */,
F1134EA91F3E2BA700B73467 /* Store */,
CB2A7EF028410DF700885F67 /* PixelEvent.swift */,
BDC234F62B27F51100D3C798 /* UniquePixel.swift */,
853A717520F62FE800FE60BC /* Pixel.swift */,
1E05D1D729C46EDA00BF9A1F /* TimedPixel.swift */,
1E05D1D529C46EBB00BF9A1F /* DailyPixel.swift */,
Expand Down Expand Up @@ -6875,6 +6878,7 @@
37445F972A155F7C0029F789 /* SyncDataProviders.swift in Sources */,
EE9D68DE2AE2A65600B55EF4 /* UserDefaults+NetworkProtection.swift in Sources */,
CB258D1F29A52B2500DEBA24 /* Configuration.swift in Sources */,
BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */,
9847C00027A2DDBB00DB07AA /* AppPrivacyConfigurationDataProvider.swift in Sources */,
F143C3281E4A9A0E00CFDE3A /* StringExtension.swift in Sources */,
85449EFB23FDA0BC00512AAF /* UserDefaultsPropertyWrapper.swift in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/NetworkProtectionTunnelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ final class NetworkProtectionTunnelController: TunnelController {

do {
try tunnelManager.connection.startVPNTunnel(options: options)
UniquePixel.fire(pixel: .networkProtectionNewUser) { error in
guard error != nil else { return }
VPNSettings(defaults: .networkProtectionGroupDefaults).vpnFirstEnabled = Pixel.Event.networkProtectionNewUser.lastFireDate(
uniquePixelStorage: UniquePixel.storage
)
}
} catch {
Pixel.fire(pixel: .networkProtectionActivationRequestFailed, error: error)
throw error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,33 @@ final class NetworkProtectionPacketTunnelProvider: PacketTunnelProvider {
private static var packetTunnelProviderEvents: EventMapping<PacketTunnelProvider.Event> = .init { event, _, _, _ in
switch event {
case .userBecameActive:
DailyPixel.fire(pixel: .networkProtectionActiveUser)
case .reportLatency, .reportTunnelFailure, .reportConnectionAttempt:
// TODO: Fire these pixels
break
let settings = VPNSettings(defaults: .networkProtectionGroupDefaults)
DailyPixel.fire(pixel: .networkProtectionActiveUser,
withAdditionalParameters: ["cohort": UniquePixel.dateString(for: settings.vpnFirstEnabled)])
case .reportConnectionAttempt(attempt: let attempt):
switch attempt {
case .connecting:
DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptConnecting)
case .success:
DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptSuccess)
case .failure:
DailyPixel.fireDailyAndCount(pixel: .networkProtectionEnableAttemptFailure)
}
case .reportTunnelFailure(result: let result):
switch result {
case .failureDetected:
DailyPixel.fireDailyAndCount(pixel: .networkProtectionTunnelFailureDetected)
case .failureRecovered:
DailyPixel.fireDailyAndCount(pixel: .networkProtectionTunnelFailureRecovered)
}
case .reportLatency(result: let result):
switch result {
case .error:
DailyPixel.fire(pixel: .networkProtectionLatencyError)
case .quality(let quality):
guard quality != .unknown else { return }
DailyPixel.fireDailyAndCount(pixel: .networkProtectionLatency(quality: quality))
}
case .rekeyCompleted:
Pixel.fire(pixel: .networkProtectionRekeyCompleted)
}
Expand Down

0 comments on commit 8697128

Please sign in to comment.