@@ -6,13 +6,17 @@ final class AlamofireNetworkErrorHandler {
66 private let queue = DispatchQueue ( label: " com.networkingcore.errorhandler " , attributes: . concurrent)
77 private let userDefaults : UserDefaults
88 private let credentials : Credentials ?
9+ private let notificationCenter : NotificationCenter
910
1011 private var _appPasswordFailures : [ Int64 : Int ] = [ : ]
1112 private var _retriedJetpackRequests : [ RetriedJetpackRequest ] = [ ]
1213
13- init ( credentials: Credentials ? , userDefaults: UserDefaults = . standard) {
14+ init ( credentials: Credentials ? ,
15+ userDefaults: UserDefaults = . standard,
16+ notificationCenter: NotificationCenter = . default) {
1417 self . credentials = credentials
1518 self . userDefaults = userDefaults
19+ self . notificationCenter = notificationCenter
1620 }
1721
1822 // MARK: - Thread-safe property access
@@ -41,8 +45,9 @@ final class AlamofireNetworkErrorHandler {
4145
4246 // MARK: - Public interface
4347
44- func resetFailureCount ( for siteID: Int64 ) {
48+ func prepareAppPasswordSupport ( for siteID: Int64 ) {
4549 appPasswordFailures. removeValue ( forKey: siteID)
50+ notificationCenter. post ( name: . JetpackSiteEligibleForAppPasswordSupport, object: siteID)
4651 }
4752
4853 func shouldRetryJetpackRequest( originalRequest: URLRequestConvertible ,
@@ -51,13 +56,14 @@ final class AlamofireNetworkErrorHandler {
5156 guard let error = failure,
5257 let request = originalRequest as? JetpackRequest ,
5358 convertedRequest is RESTRequest ,
59+ let convertedURLRequest = try ? convertedRequest. asURLRequest ( ) ,
5460 case . some( . wpcom) = self . credentials else {
5561 return false
5662 }
5763
5864 let isExpectedError : Bool = {
5965 switch error {
60- case AFError . requestAdaptationFailed :
66+ case AFError . requestRetryFailed :
6167 return true
6268 case _ as NetworkError :
6369 return true
@@ -69,6 +75,7 @@ final class AlamofireNetworkErrorHandler {
6975 if isExpectedError {
7076 let retriedRequest = RetriedJetpackRequest ( request: request, error: error)
7177 retriedJetpackRequests. append ( retriedRequest)
78+ logRequestFailure ( request: convertedURLRequest, error: error)
7279 return true
7380 }
7481 return false
@@ -86,7 +93,7 @@ final class AlamofireNetworkErrorHandler {
8693
8794 guard let index = retriedRequestIndex else { return }
8895
89- let retriedRequest = retriedJetpackRequests [ index]
96+ let retriedRequest = retriedJetpackRequests. remove ( at : index)
9097
9198 if failure == nil {
9299 let siteID = retriedRequest. request. siteID
@@ -95,19 +102,27 @@ final class AlamofireNetworkErrorHandler {
95102 case NetworkError . unacceptableStatusCode ( statusCode: 401 , _) ,
96103 NetworkError . unacceptableStatusCode ( statusCode: 403 , _) ,
97104 NetworkError . unacceptableStatusCode ( statusCode: 429 , _) :
98- flagSiteAsUnsupported ( for: siteID)
105+ flagSiteAsUnsupported (
106+ for: siteID,
107+ flow: . apiRequest,
108+ cause: . majorError,
109+ error: originalFailure
110+ )
99111 default :
100112 if let networkError = originalFailure as? NetworkError ,
101113 let code = networkError. errorCode,
102114 AppPasswordConstants . disabledCodes. contains ( code) {
103- flagSiteAsUnsupported ( for: siteID)
115+ flagSiteAsUnsupported (
116+ for: siteID,
117+ flow: . apiRequest,
118+ cause: . majorError,
119+ error: originalFailure
120+ )
104121 } else {
105- incrementFailureCount ( for: siteID)
122+ incrementFailureCount ( for: siteID, originalFailure : originalFailure )
106123 }
107124 }
108125 }
109-
110- retriedJetpackRequests. remove ( at: index)
111126 }
112127
113128 func handleFailureForDirectRequestIfNeeded( originalRequest: URLRequestConvertible ,
@@ -133,12 +148,24 @@ final class AlamofireNetworkErrorHandler {
133148 }
134149 }
135150
136- func flagSiteAsUnsupported( for siteID: Int64 ) {
151+ func flagSiteAsUnsupported( for siteID: Int64 , flow : RequestFlow , cause : AppPasswordFlagCause , error : Error ) {
137152 queue. sync ( flags: . barrier) {
138153 var currentList = userDefaults. applicationPasswordUnsupportedList
139154 currentList [ String ( siteID) ] = Date ( )
140155 userDefaults. applicationPasswordUnsupportedList = currentList
141156 }
157+
158+ /// Tracks error
159+ let apiErrorCode = ( error as? NetworkError ) ? . errorCode ?? error. localizedDescription
160+ let httpStatusCode = ( error as? NetworkError ) ? . responseCode ?? ( error as NSError ) . code
161+
162+ let tracksProperties : [ String : Any ] = [
163+ TracksProperty . flow. rawValue: flow. rawValue,
164+ TracksProperty . cause. rawValue: cause. rawValue,
165+ TracksProperty . apiErrorCode. rawValue: apiErrorCode,
166+ TracksProperty . httpStatusCode. rawValue: httpStatusCode
167+ ]
168+ notificationCenter. post ( name: . JetpackSiteFlaggedUnsupportedForApplicationPassword, object: tracksProperties)
142169 }
143170
144171 func siteFlaggedAsUnsupported( siteID: Int64 , unsupportedList: [ String : Date ] ) -> Bool {
@@ -156,13 +183,38 @@ final class AlamofireNetworkErrorHandler {
156183 }
157184}
158185
186+ enum RequestFlow : String {
187+ case appPasswordGeneration = " app_password_generation "
188+ case apiRequest = " api_request "
189+ }
190+
191+ enum AppPasswordFlagCause : String {
192+ case majorError = " major_error "
193+ case generalFailuresThresholdReached = " general_failures_threshold_reached "
194+ }
195+
159196// MARK: Private helpers
160197private extension AlamofireNetworkErrorHandler {
161- func incrementFailureCount( for siteID: Int64 ) {
198+ func incrementFailureCount( for siteID: Int64 , originalFailure : Error ) {
162199 let currentFailureCount = appPasswordFailures [ siteID] ?? 0
163200 let updatedCount = currentFailureCount + 1
164201 if updatedCount == AppPasswordConstants . requestFailureThreshold {
165- flagSiteAsUnsupported ( for: siteID)
202+ let flow : RequestFlow
203+ let failure : Error
204+ switch originalFailure {
205+ case AFError . requestRetryFailed( let error, _) :
206+ flow = . appPasswordGeneration
207+ failure = error
208+ default :
209+ flow = . apiRequest
210+ failure = originalFailure
211+ }
212+ flagSiteAsUnsupported (
213+ for: siteID,
214+ flow: flow,
215+ cause: . generalFailuresThresholdReached,
216+ error: failure
217+ )
166218 }
167219 appPasswordFailures [ siteID] = updatedCount
168220 }
@@ -176,9 +228,46 @@ private extension AlamofireNetworkErrorHandler {
176228 }
177229 }
178230
231+ func logRequestFailure( request: URLRequest , error: Error ) {
232+ let networkError : NetworkError ? = {
233+ switch error {
234+ case AFError . requestRetryFailed( let retryError, _) :
235+ return ( retryError as? NetworkError )
236+ case let networkError as NetworkError :
237+ return networkError
238+ default :
239+ return nil
240+ }
241+ } ( )
242+
243+ let siteURL = request. url? . host ( ) ?? " "
244+ let path = request. url? . path ( percentEncoded: false ) ?? " "
245+ let method = request. httpMethod ?? " "
246+ let apiErrorCode = networkError? . errorCode ?? error. localizedDescription
247+ let httpCode = networkError? . responseCode ?? ( error as NSError ) . code
248+
249+ DDLogError (
250+ """
251+ ⛔️ Request failed using Application Passwords for Jetpack Site:
252+ - Site URL: \( siteURL)
253+ - Path: \( path)
254+ - Method: \( method)
255+ - Error: HTTP status code \( httpCode)
256+ - Error message: \( apiErrorCode)
257+ """
258+ )
259+ }
260+
179261 enum Constants {
180262 static let flagRefreshDuration : Double = 60 * 60 * 24 * 14 // flag can be reset after 14 days.
181263 }
264+
265+ enum TracksProperty : String {
266+ case flow
267+ case cause
268+ case apiErrorCode = " api_error_code "
269+ case httpStatusCode = " http_status_code "
270+ }
182271}
183272/// Helper type to keep track of retried requests with accompanied error
184273struct RetriedJetpackRequest {
0 commit comments