diff --git a/CHANGELOG.md b/CHANGELOG.md index 196062d1ba..3b497aa4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Braintree iOS SDK Release Notes +## unreleased +* BraintreeCore + * Fix analytics bug where sessionID value in analytics payload was inaccurate; send separate FPTI POST requests per unique sessionID + ## 6.23.4 (2024-09-24) * BraintreePayPal * Send `isVaultRequest` for App Switch events to PayPal's analytics service (FPTI) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift index 7d8344dba3..17fd1d1c4d 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift @@ -1,27 +1,28 @@ import Foundation -/// Used to store and access our array of events in a thread-safe manner +/// Used to store and access our dictionary of events in a thread-safe manner actor BTAnalyticsEventsStorage { - private var events: [FPTIBatchData.Event] + // A list of analytic events, keyed by sessionID + private var events: [String: [FPTIBatchData.Event]] var isEmpty: Bool { events.isEmpty } - var allValues: [FPTIBatchData.Event] { + var allValues: [String: [FPTIBatchData.Event]] { events } init() { - self.events = [] + self.events = [:] } - func append(_ event: FPTIBatchData.Event) { - events.append(event) + func append(_ event: FPTIBatchData.Event, sessionID: String) { + events[sessionID] = (events[sessionID] ?? []) + [event] } - - func removeAll() { - events.removeAll() + + func removeFor(sessionID: String) { + events.removeValue(forKey: sessionID) } } diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 5d23fbca2b..7621515548 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -61,7 +61,9 @@ final class BTAnalyticsService: AnalyticsSendable { /// Exposed to be able to execute this function synchronously in unit tests func performEventRequest(with event: FPTIBatchData.Event) async { - await events.append(event) + if let apiClient { + await events.append(event, sessionID: apiClient.metadata.sessionID) + } if shouldBypassTimerQueue { await self.sendQueuedAnalyticsEvents() @@ -74,13 +76,18 @@ final class BTAnalyticsService: AnalyticsSendable { if await !events.isEmpty, let apiClient { do { let configuration = try await apiClient.fetchConfiguration() - let postParameters = await createAnalyticsEvent( - config: configuration, - sessionID: apiClient.metadata.sessionID, - events: events.allValues - ) - http?.post("v1/tracking/batch/events", parameters: postParameters) { _, _, _ in } - await events.removeAll() + + for (sessionID, eventsPerSessionID) in await events.allValues { + let postParameters = createAnalyticsEvent( + config: configuration, + sessionID: sessionID, + events: eventsPerSessionID + ) + + _ = try? await http?.post("v1/tracking/batch/events", parameters: postParameters) + + await events.removeFor(sessionID: sessionID) + } } catch { return } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 13074d106d..fef2705a89 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -126,6 +126,23 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { completion(nil, nil, error) } } + + func post( + _ path: String, + configuration: BTConfiguration? = nil, + parameters: Encodable, + headers: [String: String]? = nil + ) async throws -> (BTJSON?, HTTPURLResponse?) { + try await withCheckedThrowingContinuation { continuation in + post(path, configuration: configuration, parameters: parameters, headers: headers) { body, response, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (body, response)) + } + } + } + } // MARK: - HTTP Method Helpers diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index da402f5378..efe7bd20b1 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -47,6 +47,25 @@ final class BTAnalyticsService_Tests: XCTestCase { self.validateMetadataParameters(mockAnalyticsHTTP.lastRequestParameters) } + func testSendAnalyticsEvent_whenMultipleSessionIDs_sendsMultiplePOSTs() async { + let stubAPIClient: MockAPIClient = stubbedAPIClientWithAnalyticsURL("test://do-not-send.url") + let mockAnalyticsHTTP = FakeHTTP.fakeHTTP() + let sut = BTAnalyticsService.shared + sut.setAPIClient(stubAPIClient) + sut.http = mockAnalyticsHTTP + + // Send events associated with 1st sessionID + stubAPIClient.metadata.sessionID = "session-id-1" + await sut.performEventRequest(with: FPTIBatchData.Event(eventName: "event1")) + + // Send events associated with 2nd sessionID + stubAPIClient.metadata.sessionID = "session-id-2" + sut.shouldBypassTimerQueue = true + await sut.performEventRequest(with: FPTIBatchData.Event(eventName: "event2")) + + XCTAssertEqual(mockAnalyticsHTTP.POSTRequestCount, 2) + } + // MARK: - Helper Functions func stubbedAPIClientWithAnalyticsURL(_ analyticsURL: String? = nil) -> MockAPIClient {