Skip to content

Commit

Permalink
Add http_request_performed diagnostics event (#3897)
Browse files Browse the repository at this point in the history
### Description
This adds a new diagnostics event `http_request_performed` with some
useful information we would like to track about the http requests
performed to our servers.
  • Loading branch information
tonidero authored May 16, 2024
1 parent 1f16a59 commit bb8066d
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 27 deletions.
5 changes: 5 additions & 0 deletions Sources/Diagnostics/DiagnosticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ extension DiagnosticsEvent {
enum DiagnosticsPropertyKey: String, Codable {

case verificationResultKey
case endpointNameKey
case responseTimeMillisKey
case successfulKey
case responseCodeKey
case eTagHitKey

}

Expand Down
32 changes: 32 additions & 0 deletions Sources/Diagnostics/DiagnosticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ protocol DiagnosticsTrackerType {
@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
func trackCustomerInfoVerificationResultIfNeeded(_ customerInfo: CustomerInfo) async

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
// swiftlint:disable:next function_parameter_count
func trackHttpRequestPerformed(endpointName: String,
responseTime: TimeInterval,
wasSuccessful: Bool,
responseCode: Int,
resultOrigin: HTTPResponseOrigin?,
verificationResult: VerificationResult) async

}

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
Expand Down Expand Up @@ -56,6 +65,29 @@ final class DiagnosticsTracker: DiagnosticsTrackerType {
await track(event)
}

// swiftlint:disable:next function_parameter_count
func trackHttpRequestPerformed(endpointName: String,
responseTime: TimeInterval,
wasSuccessful: Bool,
responseCode: Int,
resultOrigin: HTTPResponseOrigin?,
verificationResult: VerificationResult) async {
await track(
DiagnosticsEvent(
eventType: DiagnosticsEvent.EventType.httpRequestPerformed,
properties: [
.endpointNameKey: AnyEncodable(endpointName),
.responseTimeMillisKey: AnyEncodable(responseTime * 1000),
.successfulKey: AnyEncodable(wasSuccessful),
.responseCodeKey: AnyEncodable(responseCode),
.eTagHitKey: AnyEncodable(resultOrigin == .cache),
.verificationResultKey: AnyEncodable(verificationResult.name)
],
timestamp: self.dateProvider.now()
)
)
}

}

@available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *)
Expand Down
10 changes: 10 additions & 0 deletions Sources/Diagnostics/Networking/DiagnosticsEventsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ private extension DiagnosticsEvent.DiagnosticsPropertyKey {
switch self {
case .verificationResultKey:
return "verification_result"
case .endpointNameKey:
return "endpoint_name"
case .responseTimeMillisKey:
return "response_time_millis"
case .successfulKey:
return "successful"
case .responseCodeKey:
return "response_code"
case .eTagHitKey:
return "etag_hit"
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Identity/CustomerInfoManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class CustomerInfoManager {

if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
if let tracker = self.diagnosticsTracker, lastSentCustomerInfo != customerInfo {
Task {
Task(priority: .background) {
await tracker.trackCustomerInfoVerificationResultIfNeeded(customerInfo)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Networking/Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ class Backend {
operationDispatcher: OperationDispatcher,
attributionFetcher: AttributionFetcher,
offlineCustomerInfoCreator: OfflineCustomerInfoCreator?,
diagnosticsTracker: DiagnosticsTrackerType?,
dateProvider: DateProvider = DateProvider()
) {
let httpClient = HTTPClient(apiKey: apiKey,
systemInfo: systemInfo,
eTagManager: eTagManager,
signing: Signing(apiKey: apiKey, clock: systemInfo.clock),
diagnosticsTracker: diagnosticsTracker,
requestTimeout: httpClientTimeout)
let config = BackendConfiguration(httpClient: httpClient,
operationDispatcher: operationDispatcher,
Expand Down
72 changes: 64 additions & 8 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ class HTTPClient {
private let eTagManager: ETagManager
private let dnsChecker: DNSCheckerType.Type
private let signing: SigningType
private let diagnosticsTracker: DiagnosticsTrackerType?
private let dateProvider: DateProvider

init(apiKey: String,
systemInfo: SystemInfo,
eTagManager: ETagManager,
signing: SigningType,
diagnosticsTracker: DiagnosticsTrackerType?,
dnsChecker: DNSCheckerType.Type = DNSChecker.self,
requestTimeout: TimeInterval = Configuration.networkTimeoutDefault) {
requestTimeout: TimeInterval = Configuration.networkTimeoutDefault,
dateProvider: DateProvider = DateProvider()) {
let config = URLSessionConfiguration.ephemeral
config.httpMaximumConnectionsPerHost = 1
config.timeoutIntervalForRequest = requestTimeout
Expand All @@ -49,10 +53,12 @@ class HTTPClient {
self.systemInfo = systemInfo
self.eTagManager = eTagManager
self.signing = signing
self.diagnosticsTracker = diagnosticsTracker
self.dnsChecker = dnsChecker
self.timeout = requestTimeout
self.apiKey = apiKey
self.authHeaders = HTTPClient.authorizationHeader(withAPIKey: apiKey)
self.dateProvider = dateProvider
}

/// - Parameter verificationMode: if `nil`, this will default to `SystemInfo.responseVerificationMode`
Expand Down Expand Up @@ -290,11 +296,13 @@ private extension HTTPClient {
}

/// - Returns: `nil` if the request must be retried
// swiftlint:disable:next function_parameter_count
func parse(urlResponse: URLResponse?,
request: Request,
urlRequest: URLRequest,
data: Data?,
error networkError: Error?) -> VerifiedHTTPResponse<Data>.Result? {
error networkError: Error?,
requestStartTime: Date) -> VerifiedHTTPResponse<Data>.Result? {
if let networkError = networkError {
return .failure(NetworkError(networkError, dnsChecker: self.dnsChecker))
}
Expand All @@ -313,15 +321,17 @@ private extension HTTPClient {
return self.createVerifiedResponse(request: request,
urlRequest: urlRequest,
data: dataIfAvailable,
response: httpURLResponse)
response: httpURLResponse,
requestStartTime: requestStartTime)
}

/// - Returns `Result<VerifiedHTTPResponse<Data>, NetworkError>?`
private func createVerifiedResponse(
request: Request,
urlRequest: URLRequest,
data: Data?,
response httpURLResponse: HTTPURLResponse
response httpURLResponse: HTTPURLResponse,
requestStartTime: Date
) -> VerifiedHTTPResponse<Data>.Result? {
#if DEBUG
let requestHeaders: HTTPClient.RequestHeaders
Expand All @@ -338,7 +348,7 @@ private extension HTTPClient {
let requestHeaders = request.headers
#endif

return Result
let result = Result
.success(data)
.mapToResponse(response: httpURLResponse, request: request.httpRequest)
// Verify response
Expand Down Expand Up @@ -375,21 +385,30 @@ private extension HTTPClient {
}
.asOptionalResult?
.convertUnsuccessfulResponseToError()

self.trackHttpRequestPerformedIfNeeded(request: request,
requestStartTime: requestStartTime,
result: result)

return result
}

// swiftlint:disable:next function_parameter_count
func handle(urlResponse: URLResponse?,
request: Request,
urlRequest: URLRequest,
data: Data?,
error networkError: Error?) {
error networkError: Error?,
requestStartTime: Date) {
RCTestAssertNotMainThread()

let response = self.parse(
urlResponse: urlResponse,
request: request,
urlRequest: urlRequest,
data: data,
error: networkError
error: networkError,
requestStartTime: requestStartTime
)

if let response = response {
Expand Down Expand Up @@ -466,12 +485,15 @@ private extension HTTPClient {

Logger.debug(Strings.network.api_request_started(request.httpRequest))

let requestStartTime = self.dateProvider.now()

let task = self.session.dataTask(with: urlRequest) { (data, urlResponse, error) -> Void in
self.handle(urlResponse: urlResponse,
request: request,
urlRequest: urlRequest,
data: data,
error: error)
error: error,
requestStartTime: requestStartTime)
}
task.resume()
}
Expand Down Expand Up @@ -518,6 +540,40 @@ private extension HTTPClient {
return self.signing
}

private func trackHttpRequestPerformedIfNeeded(request: Request,
requestStartTime: Date,
result: Result<VerifiedHTTPResponse<Data>, NetworkError>?) {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
guard let diagnosticsTracker = self.diagnosticsTracker, let result else { return }
let responseTime = self.dateProvider.now().timeIntervalSince(requestStartTime)
let requestPathName = request.httpRequest.path.name
Task(priority: .background) {
switch result {
case let .success(response):
let httpStatusCode = response.httpStatusCode.rawValue
let verificationResult = response.verificationResult
await diagnosticsTracker.trackHttpRequestPerformed(endpointName: requestPathName,
responseTime: responseTime,
wasSuccessful: true,
responseCode: httpStatusCode,
resultOrigin: response.origin,
verificationResult: verificationResult)
case let .failure(error):
var responseCode = -1
if case let .errorResponse(_, code, _) = error {
responseCode = code.rawValue
}
await diagnosticsTracker.trackHttpRequestPerformed(endpointName: requestPathName,
responseTime: responseTime,
wasSuccessful: false,
responseCode: responseCode,
resultOrigin: nil,
verificationResult: .notRequested)
}
}
}
}

}

// MARK: - Extensions
Expand Down
35 changes: 18 additions & 17 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
let purchasedProductsFetcher = OfflineCustomerInfoCreator.createPurchasedProductsFetcherIfAvailable()
let transactionFetcher = StoreKit2TransactionFetcher()

let diagnosticsFileHandler: DiagnosticsFileHandlerType? = {
guard diagnosticsEnabled, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { return nil }
return DiagnosticsFileHandler()
}()

let diagnosticsTracker: DiagnosticsTrackerType? = {
if let handler = diagnosticsFileHandler, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
return DiagnosticsTracker(diagnosticsFileHandler: handler)
} else {
if diagnosticsEnabled {
Logger.error(Strings.diagnostics.could_not_create_diagnostics_tracker)
}
}
return nil
}()

let backend = Backend(
apiKey: apiKey,
systemInfo: systemInfo,
Expand All @@ -310,7 +326,8 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
with: purchasedProductsFetcher,
productEntitlementMappingFetcher: deviceCache,
observerMode: observerMode
)
),
diagnosticsTracker: diagnosticsTracker
)

let paymentQueueWrapper: EitherPaymentQueueWrapper = systemInfo.storeKit2Setting.shouldOnlyUseStoreKit2
Expand Down Expand Up @@ -347,22 +364,6 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void
api: backend.offlineEntitlements,
systemInfo: systemInfo)

let diagnosticsFileHandler: DiagnosticsFileHandlerType? = {
guard diagnosticsEnabled, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) else { return nil }
return DiagnosticsFileHandler()
}()

let diagnosticsTracker: DiagnosticsTrackerType? = {
if let handler = diagnosticsFileHandler, #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
return DiagnosticsTracker(diagnosticsFileHandler: handler)
} else {
if diagnosticsEnabled {
Logger.error(Strings.diagnostics.could_not_create_diagnostics_tracker)
}
}
return nil
}()

let customerInfoManager: CustomerInfoManager
if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
customerInfoManager = CustomerInfoManager(offlineEntitlementsManager: offlineEntitlementsManager,
Expand Down
23 changes: 23 additions & 0 deletions Tests/UnitTests/Diagnostics/DiagnosticsTrackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ class DiagnosticsTrackerTests: TestCase {
]
}

// MARK: - http request performed

func testTracksHttpRequestPerformedWithExpectedParameters() async {
await self.tracker.trackHttpRequestPerformed(endpointName: "mock_endpoint",
responseTime: 50,
wasSuccessful: true,
responseCode: 200,
resultOrigin: .cache,
verificationResult: .verified)
let entries = await self.handler.getEntries()
expect(entries) == [
.init(eventType: .httpRequestPerformed,
properties: [
.endpointNameKey: AnyEncodable("mock_endpoint"),
.responseTimeMillisKey: AnyEncodable(50000),
.successfulKey: AnyEncodable(true),
.responseCodeKey: AnyEncodable(200),
.eTagHitKey: AnyEncodable(true),
.verificationResultKey: AnyEncodable("VERIFIED")],
timestamp: Self.eventTimestamp1)
]
}

// MARK: - empty diagnostics file when too big

func testTrackingEventClearsDiagnosticsFileIfTooBig() async throws {
Expand Down
5 changes: 5 additions & 0 deletions Tests/UnitTests/Mocks/MockBackendConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ class MockBackendConfiguration: BackendConfiguration {
init() {
let systemInfo = MockSystemInfo(finishTransactions: false)
let mockAPIKey = "mockAPIKey"
var diagnosticsTracker: DiagnosticsTrackerType?
if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) {
diagnosticsTracker = MockDiagnosticsTracker()
}
let httpClient = MockHTTPClient(apiKey: mockAPIKey,
systemInfo: systemInfo,
eTagManager: MockETagManager(),
diagnosticsTracker: diagnosticsTracker,
requestTimeout: 7)

super.init(
Expand Down
16 changes: 16 additions & 0 deletions Tests/UnitTests/Mocks/MockDiagnosticsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,20 @@ class MockDiagnosticsTracker: DiagnosticsTrackerType {
trackedCustomerInfo.append(customerInfo)
}

private(set) var trackedHttpRequestPerformedParams: [
// swiftlint:disable:next large_tuple
(String, TimeInterval, Bool, Int, HTTPResponseOrigin?, VerificationResult)
] = []
// swiftlint:disable:next function_parameter_count
func trackHttpRequestPerformed(endpointName: String,
responseTime: TimeInterval,
wasSuccessful: Bool,
responseCode: Int,
resultOrigin: HTTPResponseOrigin?,
verificationResult: VerificationResult) async {
self.trackedHttpRequestPerformedParams.append(
(endpointName, responseTime, wasSuccessful, responseCode, resultOrigin, verificationResult)
)
}

}
Loading

0 comments on commit bb8066d

Please sign in to comment.