Skip to content

Commit

Permalink
fix: catch network error & add the current timeout setting in the Tim…
Browse files Browse the repository at this point in the history
…eoutErrorMetricsEvent
  • Loading branch information
duyhungtnn committed Sep 1, 2023
1 parent 8df5ae3 commit 1da9f99
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 78 deletions.
131 changes: 79 additions & 52 deletions Bucketeer/Sources/Internal/Event/EventInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,23 +178,35 @@ final class EventInteractorImpl: EventInteractor {
}

func trackFetchEvaluationsFailure(featureTag: String, error: BKTError) throws {
let metrics = metricsEvent(apiId: .getEvaluations, labels: ["tag": featureTag], error: error)
let eventData = error.toMetricsEventData(
apiId: .getEvaluations,
labels: ["tag": featureTag],
currentTimeSeconds: clock.currentTimeSeconds,
sdkVersion: sdkVersion,
metadata: metadata
)
try trackMetricsEvent(events: [
.init(
id: idGenerator.id(),
event: metrics,
event: eventData,
type: .metrics
)
])
}

func trackRegisterEventsFailure(error: BKTError) throws {
// note: using the same tag in BKConfig.featureTag
let metrics = metricsEvent(apiId: .registerEvents, labels: ["tag": featureTag], error: error)
let eventData = error.toMetricsEventData(
apiId: .registerEvents,
labels: ["tag": featureTag],
currentTimeSeconds: clock.currentTimeSeconds,
sdkVersion: sdkVersion,
metadata: metadata
)
try trackMetricsEvent(events: [
.init(
id: idGenerator.id(),
event: metrics,
event: eventData,
type: .metrics
)
])
Expand Down Expand Up @@ -271,54 +283,6 @@ final class EventInteractorImpl: EventInteractor {
}
}

private func metricsEvent(apiId: ApiId, labels: [String: String], error: BKTError) -> EventData {
let metricsEventData: MetricsEventData
let metricsEventType: MetricsEventType
switch error {
case .timeout:
metricsEventData = .timeoutError(.init(apiId: apiId, labels: labels))
metricsEventType = .timeoutError
case .network:
metricsEventData = .networkError(.init(apiId: apiId, labels: labels))
metricsEventType = .networkError
case .badRequest:
metricsEventData = .badRequestError(.init(apiId: apiId, labels: labels))
metricsEventType = .badRequestError
case .unauthorized:
metricsEventData = .unauthorizedError(.init(apiId: apiId, labels: labels))
metricsEventType = .unauthorizedError
case .forbidden:
metricsEventData = .forbiddenError(.init(apiId: apiId, labels: labels))
metricsEventType = .forbiddenError
case .notFound:
metricsEventData = .notFoundError(.init(apiId: apiId, labels: labels))
metricsEventType = .notFoundError
case .clientClosed:
metricsEventData = .clientClosedError(.init(apiId: apiId, labels: labels))
metricsEventType = .clientClosedError
case .unavailable:
metricsEventData = .unavailableError(.init(apiId: apiId, labels: labels))
metricsEventType = .unavailableError
case .apiServer:
metricsEventData = .internalServerError(.init(apiId: apiId, labels: labels))
metricsEventType = .internalServerError
case .unknownServer:
metricsEventData = .unknownError(.init(apiId: apiId, labels: labels))
metricsEventType = .unknownError
default:
metricsEventData = .internalSdkError(.init(apiId: apiId, labels: labels))
metricsEventType = .internalError
}
return .metrics(.init(
timestamp: clock.currentTimeSeconds,
event: metricsEventData,
type: metricsEventType,
sourceId: .ios,
sdk_version: sdkVersion,
metadata: metadata
))
}

private func updateEventsAndNotify() {
do {
let events = try eventDao.getEvents()
Expand Down Expand Up @@ -369,3 +333,66 @@ extension Event {
}
}
}

extension BKTError {
func toMetricsEventData(apiId: ApiId, labels: [String: String], currentTimeSeconds: Int64, sdkVersion: String, metadata: [String: String]?) ->
EventData {
let error = self
let metricsEventData: MetricsEventData
let metricsEventType: MetricsEventType
switch error {
case .timeout(_, _, let timeoutMillis):
// https://github.com/bucketeer-io/ios-client-sdk/issues/16
// Pass the current timeout setting in seconds via labels.
let timeoutSecs : Double = Double(timeoutMillis)/1000
metricsEventData = .timeoutError(
.init(
apiId: apiId,
labels: labels.merging(
["timeout":"\(timeoutSecs)"]
, uniquingKeysWith: { (first, _) in first }
)
)
)
metricsEventType = .timeoutError
case .network:
metricsEventData = .networkError(.init(apiId: apiId, labels: labels))
metricsEventType = .networkError
case .badRequest:
metricsEventData = .badRequestError(.init(apiId: apiId, labels: labels))
metricsEventType = .badRequestError
case .unauthorized:
metricsEventData = .unauthorizedError(.init(apiId: apiId, labels: labels))
metricsEventType = .unauthorizedError
case .forbidden:
metricsEventData = .forbiddenError(.init(apiId: apiId, labels: labels))
metricsEventType = .forbiddenError
case .notFound:
metricsEventData = .notFoundError(.init(apiId: apiId, labels: labels))
metricsEventType = .notFoundError
case .clientClosed:
metricsEventData = .clientClosedError(.init(apiId: apiId, labels: labels))
metricsEventType = .clientClosedError
case .unavailable:
metricsEventData = .unavailableError(.init(apiId: apiId, labels: labels))
metricsEventType = .unavailableError
case .apiServer:
metricsEventData = .internalServerError(.init(apiId: apiId, labels: labels))
metricsEventType = .internalServerError
case .illegalArgument, .illegalState:
metricsEventData = .internalSdkError(.init(apiId: apiId, labels: labels))
metricsEventType = .internalError
case .unknownServer, .unknown:
metricsEventData = .unknownError(.init(apiId: apiId, labels: labels))
metricsEventType = .unknownError
}
return .metrics(.init(
timestamp: currentTimeSeconds,
event: metricsEventData,
type: metricsEventType,
sourceId: .ios,
sdk_version: sdkVersion,
metadata: metadata
))
}
}
16 changes: 14 additions & 2 deletions Bucketeer/Sources/Internal/Remote/ApiClientImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ final class ApiClientImpl: ApiClient {
sourceId: .ios
)
let featureTag = self.featureTag
let timeoutMillisValue = timeoutMillis ?? defaultRequestTimeoutMills
logger?.debug(message: "[API] Fetch Evaluation: \(requestBody)")
send(
requestBody: requestBody,
path: "get_evaluations",
timeoutMillis: timeoutMillis ?? defaultRequestTimeoutMills,
timeoutMillis: timeoutMillisValue,
completion: { (result: Result<(GetEvaluationsResponse, URLResponse), Error>) in
switch result {
case .success((var response, let urlResponse)):
Expand All @@ -61,7 +62,7 @@ final class ApiClientImpl: ApiClient {
response.featureTag = featureTag
completion?(.success(response))
case .failure(let error):
completion?(.failure(error: .init(error: error), featureTag: featureTag))
completion?(.failure(error: .init(error: error).copyWith(timeoutMillis: timeoutMillisValue), featureTag: featureTag))
}
}
)
Expand Down Expand Up @@ -214,3 +215,14 @@ fileprivate extension UnixTimestamp {
return Date(timeIntervalSince1970: TimeInterval(self / 1_000)) // must take a millisecond-precise Unix timestamp
}
}

extension BKTError {
func copyWith(timeoutMillis: Int64) -> BKTError {
switch self {
case .timeout(let m, let e, _):
return .timeout(message: m, error: e, timeoutMillis: timeoutMillis)
default:
return self
}
}
}
56 changes: 48 additions & 8 deletions Bucketeer/Sources/Public/BKTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public enum BKTError: Error, Equatable {
case apiServer(message: String)

// network errors
case timeout(message: String, error: Error)
case timeout(message: String, error: Error, timeoutMillis: Int64)
case network(message: String, error: Error)

// sdk errors
Expand All @@ -33,8 +33,9 @@ public enum BKTError: Error, Equatable {
(.illegalArgument(let m1), .illegalArgument(let m2)),
(.illegalState(let m1), .illegalState(let m2)):
return m1 == m2
case (.timeout(let m1, _), .timeout(let m2, _)),
(.network(let m1, _), .network(let m2, _)),
case (.timeout(let m1, _, let t1), .timeout(let m2, _, let t2)):
return t1 == t2 && m1 == m2
case (.network(let m1, _), .network(let m2, _)),
(.unknownServer(let m1, _), .unknownServer(let m2, _)),
(.unknown(let m1, _), .unknown(let m2, _)):
return m1 == m2
Expand All @@ -45,6 +46,7 @@ public enum BKTError: Error, Equatable {
}

extension BKTError : LocalizedError {

internal init(error: Error) {
if let bktError = error as? BKTError {
self = bktError
Expand Down Expand Up @@ -87,9 +89,15 @@ extension BKTError : LocalizedError {
}

let nsError = error as NSError
if nsError.domain == NSURLErrorDomain,
nsError.code == NSURLErrorTimedOut {
self = .timeout(message: "Request timeout error: \(error)", error: error)
if nsError.domain == NSURLErrorDomain {
let nsErrorCode = nsError.code
if BKTError.networkErrorCodes.contains(nsErrorCode) {
self = .network(message: "Network connection error: \(error)", error: error)
} else if nsErrorCode == NSURLErrorTimedOut {
self = .timeout(message: "Request timeout error: \(error)", error: error, timeoutMillis: 0)
} else {
self = .unknown(message: "Unknown error: \(error)", error: error)
}
} else {
self = .unknown(message: "Unknown error: \(error)", error: error)
}
Expand All @@ -113,7 +121,7 @@ extension BKTError : LocalizedError {
return message
case .apiServer(message: let message):
return message
case .timeout(message: let message, _):
case .timeout(message: let message, _, _):
return message
case .network(message: let message, _):
return message
Expand Down Expand Up @@ -143,7 +151,7 @@ extension BKTError : LocalizedError {
.illegalState:
return nil

case .timeout(message: _, error: let error):
case .timeout(message: _, error: let error, _):
// note: create description for unknown error type
return "\(error)"

Expand All @@ -158,3 +166,35 @@ extension BKTError : LocalizedError {
}
}
}

extension BKTError {
// full list of NSURLError https://developer.apple.com/documentation/foundation/nserror/1448136-nserror_codes#3139076
static let networkErrorCodes = [
NSURLErrorBadURL,
NSURLErrorUnsupportedURL,
NSURLErrorNotConnectedToInternet,
NSURLErrorNetworkConnectionLost,
NSURLErrorCannotFindHost,
NSURLErrorCannotConnectToHost,
NSURLErrorDNSLookupFailed,
// Router, gateway error
NSURLErrorHTTPTooManyRedirects,
NSURLErrorRedirectToNonExistentLocation,
// SSL error
NSURLErrorAppTransportSecurityRequiresSecureConnection,
NSURLErrorSecureConnectionFailed,
NSURLErrorServerCertificateHasBadDate,
NSURLErrorServerCertificateUntrusted,
NSURLErrorServerCertificateHasUnknownRoot,
NSURLErrorServerCertificateNotYetValid,
NSURLErrorClientCertificateRejected,
NSURLErrorClientCertificateRequired,
// Data network errors 3G,4G...
NSURLErrorResourceUnavailable,
NSURLErrorCannotLoadFromNetwork,
NSURLErrorInternationalRoamingOff,
NSURLErrorCallIsActive,
NSURLErrorDataNotAllowed,
NSURLErrorRequestBodyStreamExhausted
]
}
19 changes: 14 additions & 5 deletions BucketeerTests/BKTClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,14 @@ final class BKTClientTests: XCTestCase {
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 3
var count = 0
let expectedTimeoutMillis: Int64 = 3500
let dataModule = MockDataModule(
userHolder: .init(user: .mock1),
apiClient: MockApiClient(getEvaluationsHandler: { (user, userEvaluationsId, timeoutMillis, handler) in
XCTAssertEqual(user, .mock1)
XCTAssertEqual(userEvaluationsId, "")
XCTAssertEqual(timeoutMillis, nil)
handler?(.failure(error: .timeout(message: "timeout", error: NSError()), featureTag: "feature"))
XCTAssertEqual(timeoutMillis, expectedTimeoutMillis)
handler?(.failure(error: .timeout(message: "timeout", error: NSError(), timeoutMillis: timeoutMillis ?? 0), featureTag: "feature"))
expectation.fulfill()
}),
eventDao: MockEventDao(addEventsHandler: { events in
Expand All @@ -199,7 +200,15 @@ final class BKTClientTests: XCTestCase {
id: "mock1",
event: .metrics(.init(
timestamp: 1,
event: .timeoutError(.init(apiId: .getEvaluations, labels: ["tag": "feature"])),
event: .timeoutError(
.init(
apiId: .getEvaluations,
labels: [
"tag": "feature",
"timeout":"\(3.5)"
]
)
),
type: .timeoutError,
sourceId: .ios,
sdk_version: "0.0.2",
Expand All @@ -222,8 +231,8 @@ final class BKTClientTests: XCTestCase {
clock: MockClock(timestamp: 1)
)
let client = BKTClient(dataModule: dataModule, dispatchQueue: .global())
client.fetchEvaluations(timeoutMillis: nil) { error in
XCTAssertEqual(error, .timeout(message: "timeout", error: NSError()))
client.fetchEvaluations(timeoutMillis: expectedTimeoutMillis) { error in
XCTAssertEqual(error, .timeout(message: "timeout", error: NSError(), timeoutMillis: expectedTimeoutMillis))
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.1)
Expand Down
Loading

0 comments on commit 1da9f99

Please sign in to comment.