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

Network Protection Error Pixels #2019

Merged
merged 15 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion Core/DailyPixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ public final class DailyPixel {
}

private static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)!


/// Sends a given Pixel once per day.
/// This means a pixel will get sent twice the first time it is called per-day.
/// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected.
/// Does not append any suffix unlike the alternative function below
public static func fire(pixel: Pixel.Event,
withAdditionalParameters params: [String: String] = [:],
onComplete: @escaping (Swift.Error?) -> Void = { _ in }) {
Expand All @@ -56,6 +60,27 @@ public final class DailyPixel {
onComplete(Error.alreadyFired)
}
}

/// Sends a given Pixel once per day with a `_d` suffix, in addition to every time it is called with a `_c` suffix.
/// This means a pixel will get sent twice the first time it is called per-day, and subsequent calls that day will only send the `_c` variant.
/// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected.
public static func fireDailyAndCount(pixel: Pixel.Event,
error: Swift.Error? = nil,
withAdditionalParameters params: [String: String] = [:],
onDailyComplete: @escaping (Swift.Error?) -> Void = { _ in },
onCountComplete: @escaping (Swift.Error?) -> Void = { _ in }) {
if !pixel.hasBeenFiredToday(dailyPixelStorage: storage) {
Pixel.fire(pixelNamed: pixel.name + "_d", withAdditionalParameters: params, onComplete: onDailyComplete)
} else {
onDailyComplete(Error.alreadyFired)
}
updatePixelLastFireDate(pixel: pixel)
var newParams = params
if let error {
newParams.appendErrorPixelParams(error: error)
}
Pixel.fire(pixelNamed: pixel.name + "_c", withAdditionalParameters: newParams, onComplete: onCountComplete)
}

private static func updatePixelLastFireDate(pixel: Pixel.Event) {
storage.set(Date(), forKey: pixel.name)
Expand Down
67 changes: 48 additions & 19 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ public struct PixelParameters {
public static let message = "message"
public static let sheetResult = "success"

// Network Protection
public static let keychainFieldName = "fieldName"
public static let keychainErrorCode = errorCode
public static let function = "function"
public static let line = "line"
public static let reason = "reason"

// Return user
public static let returnUserErrorCode = "error_code"
public static let returnUserOldATB = "old_atb"
Expand Down Expand Up @@ -146,7 +153,24 @@ public class Pixel {
withHeaders headers: APIRequest.Headers = APIRequest.Headers(),
includedParameters: [QueryParameters] = [.atb, .appVersion],
onComplete: @escaping (Error?) -> Void = { _ in }) {

fire(
pixelNamed: pixel.name,
forDeviceType: deviceType,
withAdditionalParameters: params,
allowedQueryReservedCharacters: allowedQueryReservedCharacters,
withHeaders: headers,
includedParameters: includedParameters,
onComplete: onComplete
)
}

public static func fire(pixelNamed pixelName: String,
forDeviceType deviceType: UIUserInterfaceIdiom? = UIDevice.current.userInterfaceIdiom,
withAdditionalParameters params: [String: String] = [:],
allowedQueryReservedCharacters: CharacterSet? = nil,
withHeaders headers: APIRequest.Headers = APIRequest.Headers(),
includedParameters: [QueryParameters] = [.atb, .appVersion],
onComplete: @escaping (Error?) -> Void = { _ in }) {
var newParams = params
if includedParameters.contains(.appVersion) {
newParams[PixelParameters.appVersion] = AppVersion.shared.versionAndBuildNumber
Expand All @@ -157,24 +181,24 @@ public class Pixel {
if isInternalUser {
newParams[PixelParameters.isInternalUser] = "true"
}

let url: URL
if let deviceType = deviceType {
let formFactor = deviceType == .pad ? Constants.tablet : Constants.phone
url = URL.makePixelURL(pixelName: pixel.name,
url = URL.makePixelURL(pixelName: pixelName,
formFactor: formFactor,
includeATB: includedParameters.contains(.atb))
} else {
url = URL.makePixelURL(pixelName: pixel.name, includeATB: includedParameters.contains(.atb) )
url = URL.makePixelURL(pixelName: pixelName, includeATB: includedParameters.contains(.atb) )
}

let configuration = APIRequest.Configuration(url: url,
queryParameters: newParams,
allowedQueryReservedCharacters: allowedQueryReservedCharacters,
headers: headers)
let request = APIRequest(configuration: configuration, urlSession: .session(useMainThreadCallbackQueue: true))
request.fetch { _, error in
os_log("Pixel fired %s %s", log: .generalLog, type: .debug, pixel.name, "\(params)")
os_log("Pixel fired %s %s", log: .generalLog, type: .debug, pixelName, "\(params)")
onComplete(error)
}
}
Expand All @@ -189,20 +213,25 @@ extension Pixel {
onComplete: @escaping (Error?) -> Void = { _ in }) {
var newParams = params
if let error {
let nsError = error as NSError

newParams[PixelParameters.errorCode] = "\(nsError.code)"
newParams[PixelParameters.errorDomain] = nsError.domain

if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError {
newParams[PixelParameters.underlyingErrorCode] = "\(underlyingError.code)"
newParams[PixelParameters.underlyingErrorDomain] = underlyingError.domain
} else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber {
newParams[PixelParameters.underlyingErrorCode] = "\(sqlErrorCode.intValue)"
newParams[PixelParameters.underlyingErrorDomain] = "NSSQLiteErrorDomain"
}
newParams.appendErrorPixelParams(error: error)
}

fire(pixel: pixel, withAdditionalParameters: newParams, includedParameters: [], onComplete: onComplete)
}
}

extension Dictionary where Key == String, Value == String {
mutating func appendErrorPixelParams(error: Error) {
let nsError = error as NSError

self[PixelParameters.errorCode] = "\(nsError.code)"
self[PixelParameters.errorDomain] = nsError.domain

if let underlyingError = nsError.userInfo["NSUnderlyingError"] as? NSError {
self[PixelParameters.underlyingErrorCode] = "\(underlyingError.code)"
self[PixelParameters.underlyingErrorDomain] = underlyingError.domain
} else if let sqlErrorCode = nsError.userInfo["NSSQLiteErrorDomain"] as? NSNumber {
self[PixelParameters.underlyingErrorCode] = "\(sqlErrorCode.intValue)"
self[PixelParameters.underlyingErrorDomain] = "NSSQLiteErrorDomain"
}
}
}
91 changes: 91 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,55 @@ extension Pixel {
case appTPDBTrackerStoreFailure
case appTPCouldNotLoadDatabase

// MARK: Network Protection

case networkProtectionTunnelConfigurationNoServerRegistrationInfo
case networkProtectionTunnelConfigurationCouldNotSelectClosestServer
case networkProtectionTunnelConfigurationCouldNotGetPeerPublicKey
case networkProtectionTunnelConfigurationCouldNotGetPeerHostName
case networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange

case networkProtectionClientFailedToFetchServerList
case networkProtectionClientFailedToParseServerListResponse
case networkProtectionClientFailedToEncodeRegisterKeyRequest
case networkProtectionClientFailedToFetchRegisteredServers
case networkProtectionClientFailedToParseRegisteredServersResponse
case networkProtectionClientFailedToEncodeRedeemRequest
case networkProtectionClientInvalidInviteCode
case networkProtectionClientFailedToRedeemInviteCode
case networkProtectionClientFailedToParseRedeemResponse
case networkProtectionClientInvalidAuthToken

case networkProtectionServerListStoreFailedToEncodeServerList
case networkProtectionServerListStoreFailedToDecodeServerList
case networkProtectionServerListStoreFailedToWriteServerList
case networkProtectionServerListStoreFailedToReadServerList

case networkProtectionKeychainErrorFailedToCastKeychainValueToData
case networkProtectionKeychainReadError
case networkProtectionKeychainWriteError
case networkProtectionKeychainDeleteError

case networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor
case networkProtectionWireguardErrorInvalidState
case networkProtectionWireguardErrorFailedDNSResolution
case networkProtectionWireguardErrorCannotSetNetworkSettings
case networkProtectionWireguardErrorCannotStartWireguardBackend

case networkProtectionFailedToLoadFromPreferences
case networkProtectionFailedToSaveToPreferences
case networkProtectionActivationRequestFailed
case networkProtectionFailedToStartTunnel

case networkProtectionDisconnected

case networkProtectionNoAuthTokenFoundError

case networkProtectionMemoryWarning
case networkProtectionMemoryCritical

case networkProtectionUnhandledError

// MARK: remote messaging pixels

case remoteMessageShown
Expand Down Expand Up @@ -728,6 +777,48 @@ extension Pixel.Event {
case .appTPDBTrackerStoreFailure: return "m_apptp_db_tracker_store_failure"
case .appTPCouldNotLoadDatabase: return "m_apptp_could_not_load_database"

// MARK: Network Protection pixels

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"
case .networkProtectionTunnelConfigurationCouldNotGetPeerHostName: return "m_netp_tunnel_config_error_could_not_get_peer_host_name"
case .networkProtectionTunnelConfigurationCouldNotGetInterfaceAddressRange:
return "m_netp_tunnel_config_error_could_not_get_interface_address_range"
case .networkProtectionClientFailedToFetchServerList: return "m_netp_backend_api_error_failed_to_fetch_server_list"
case .networkProtectionClientFailedToParseServerListResponse: return "m_netp_backend_api_error_parsing_server_list_response_failed"
case .networkProtectionClientFailedToEncodeRegisterKeyRequest: return "m_netp_backend_api_error_encoding_register_request_body_failed"
case .networkProtectionClientFailedToFetchRegisteredServers: return "m_netp_backend_api_error_failed_to_fetch_registered_servers"
case .networkProtectionClientFailedToParseRegisteredServersResponse:
return "m_netp_backend_api_error_parsing_device_registration_response_failed"
case .networkProtectionClientFailedToEncodeRedeemRequest: return "m_netp_backend_api_error_encoding_redeem_request_body_failed"
case .networkProtectionClientInvalidInviteCode: return "m_netp_backend_api_error_invalid_invite_code"
case .networkProtectionClientFailedToRedeemInviteCode: return "m_netp_backend_api_error_failed_to_redeem_invite_code"
case .networkProtectionClientFailedToParseRedeemResponse: return "m_netp_backend_api_error_parsing_redeem_response_failed"
case .networkProtectionClientInvalidAuthToken: return "m_netp_backend_api_error_invalid_auth_token"
case .networkProtectionServerListStoreFailedToEncodeServerList: return "m_netp_storage_error_failed_to_encode_server_list"
case .networkProtectionServerListStoreFailedToDecodeServerList: return "m_netp_storage_error_failed_to_decode_server_list"
case .networkProtectionServerListStoreFailedToWriteServerList: return "m_netp_storage_error_server_list_file_system_write_failed"
case .networkProtectionServerListStoreFailedToReadServerList: return "m_netp_storage_error_server_list_file_system_read_failed"
case .networkProtectionKeychainErrorFailedToCastKeychainValueToData: return "m_netp_keychain_error_failed_to_cast_keychain_value_to_data"
case .networkProtectionKeychainReadError: return "m_netp_keychain_error_read_failed"
case .networkProtectionKeychainWriteError: return "m_netp_keychain_error_write_failed"
case .networkProtectionKeychainDeleteError: return "m_netp_keychain_error_delete_failed"
case .networkProtectionWireguardErrorCannotLocateTunnelFileDescriptor: return "m_netp_wireguard_error_cannot_locate_tunnel_file_descriptor"
case .networkProtectionWireguardErrorInvalidState: return "m_netp_wireguard_error_invalid_state"
case .networkProtectionWireguardErrorFailedDNSResolution: return "m_netp_wireguard_error_failed_dns_resolution"
case .networkProtectionWireguardErrorCannotSetNetworkSettings: return "m_netp_wireguard_error_cannot_set_network_settings"
case .networkProtectionWireguardErrorCannotStartWireguardBackend: return "m_netp_wireguard_error_cannot_start_wireguard_backend"
case .networkProtectionFailedToLoadFromPreferences: return "m_netp_network_extension_error_failed_to_load_from_preferences"
case .networkProtectionFailedToSaveToPreferences: return "m_netp_network_extension_error_failed_to_save_to_preferences"
case .networkProtectionActivationRequestFailed: return "m_netp_network_extension_error_activation_request_failed"
case .networkProtectionFailedToStartTunnel: return "m_netp_failed_to_start_tunnel"
case .networkProtectionDisconnected: return "m_netp_vpn_disconnect"
case .networkProtectionNoAuthTokenFoundError: return "m_netp_no_auth_token_found_error"
case .networkProtectionMemoryWarning: return "m_netp_vpn_memory_warning"
case .networkProtectionMemoryCritical: return "m_netp_vpn_memory_critical"
case .networkProtectionUnhandledError: return "m_netp_unhandled_error"

// MARK: remote messaging pixels

case .remoteMessageShown: return "m_remote_message_shown"
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@
EE3B226B29DE0F110082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; };
EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3B226A29DE0F110082298A /* MockInternalUserStoring.swift */; };
EE41BD192A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */; };
EE458D0D2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */; };
EE4BE0092A740BED00CD6AA8 /* ClearTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */; };
EE4FB1862A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4FB1852A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift */; };
EE4FB1882A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4FB1872A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift */; };
Expand Down Expand Up @@ -2329,6 +2330,7 @@
EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtensionAlpha.entitlements; sourceTree = "<group>"; };
EE3B98EC2A963538002F63A0 /* PacketTunnelProviderAlpha.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnelProviderAlpha.entitlements; sourceTree = "<group>"; };
EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionInviteViewModelTests.swift; sourceTree = "<group>"; };
EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventMapping+NetworkProtectionError.swift"; sourceTree = "<group>"; };
EE4BE0082A740BED00CD6AA8 /* ClearTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearTextField.swift; sourceTree = "<group>"; };
EE4FB1852A28CE7200E5CBA7 /* NetworkProtectionStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionStatusView.swift; sourceTree = "<group>"; };
EE4FB1872A28D11900E5CBA7 /* NetworkProtectionStatusViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionStatusViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4315,6 +4317,7 @@
isa = PBXGroup;
children = (
EE0153E02A6EABE0002A8B26 /* NetworkProtectionConvenienceInitialisers.swift */,
EE458D0C2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift */,
);
name = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -6047,6 +6050,7 @@
CB258D1229A4F24900DEBA24 /* ConfigurationManager.swift in Sources */,
8546A54A2A672959003929BF /* MainViewController+Email.swift in Sources */,
F4F6DFB226E6AEC100ED7E12 /* AddOrEditBookmarkViewController.swift in Sources */,
EE458D0D2AB1DA4600FC651A /* EventMapping+NetworkProtectionError.swift in Sources */,
85047C752A0D3C2900D2FF3F /* SyncSettingsViewController+Themable.swift in Sources */,
F44D279F27F331BB0037F371 /* AutofillLoginPromptViewController.swift in Sources */,
C1BF0BA529B63D7200482B73 /* AutofillLoginPromptHelper.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
},
{
"package": "TrackerRadarKit",
"repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit.git",
"repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit",
"state": {
"branch": null,
"revision": "4684440d03304e7638a2c8086895367e90987463",
Expand Down
Loading