Skip to content

Commit

Permalink
Splitting responsibilities of analytics provider (#1884)
Browse files Browse the repository at this point in the history
# Summary

Splits analytics provider responsibilities into two by moving the
conditional follow up events on its own class `EventAnalyticsProvider`
with the timer/batching logic.
This object is only created by the context if the configuration is
turned on.
The original analytics provider is now only responsible for the initial
call, is not aware of any of the events provider's lifetime and only
passes the calls to it.

# Ticket

<ticket>
COIOS-815
</ticket>

---------

Co-authored-by: Alex Guretzki <[email protected]>
  • Loading branch information
erenbesel and goergisn authored Nov 8, 2024
1 parent 0441660 commit 785fd56
Show file tree
Hide file tree
Showing 11 changed files with 433 additions and 397 deletions.
12 changes: 12 additions & 0 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@
A00167752BD164A20099A6AE /* CardKCPValidatorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00167742BD164A20099A6AE /* CardKCPValidatorsTests.swift */; };
A00167792BD274E40099A6AE /* BrazilSocialSecurityNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00167782BD274E40099A6AE /* BrazilSocialSecurityNumberValidator.swift */; };
A001677B2BD278410099A6AE /* BrazilSocialSecurityNumberValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A001677A2BD278410099A6AE /* BrazilSocialSecurityNumberValidatorTests.swift */; };
A00580A62CD8CBC300294F2E /* EventAnalyticsProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00580A52CD8CBC300294F2E /* EventAnalyticsProviderTests.swift */; };
A009837D279859DC007E68C4 /* ACHDirectDebitComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A009837C279859DC007E68C4 /* ACHDirectDebitComponentTests.swift */; };
A00F73A32B3DA38A00E9252B /* DisableStoredPaymentMethodRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00F73A22B3DA38A00E9252B /* DisableStoredPaymentMethodRequest.swift */; };
A00F73A52B3DB67B00E9252B /* AdyenSession+StoredPaymentMethodsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00F73A42B3DB67B00E9252B /* AdyenSession+StoredPaymentMethodsDelegate.swift */; };
Expand Down Expand Up @@ -367,6 +368,8 @@
A04F8C3129E596C300F3F62B /* CashAppPayDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A01D775529B250C10075BD70 /* CashAppPayDetails.swift */; };
A04F8C3229E596C300F3F62B /* CashAppPayComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A03B7C9129B0C61500FDDDFC /* CashAppPayComponent.swift */; };
A05D7212276A28EE0087F3E9 /* DocumentComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05D7211276A28EE0087F3E9 /* DocumentComponentTests.swift */; };
A06170A92CCF96B300AF388A /* EventAnalyticsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A06170A82CCF96B300AF388A /* EventAnalyticsProvider.swift */; };
A06170AB2CCFE0F600AF388A /* AnalyticsProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A06170AA2CCFE0F600AF388A /* AnalyticsProviderProtocol.swift */; };
A0644AB728A5120C00BBF4A9 /* StorePaymentMethodFieldAware.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0644AB628A5120C00BBF4A9 /* StorePaymentMethodFieldAware.swift */; };
A06B7A9A2A1CEC59008FC09D /* StoredCashAppPayPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A06B7A992A1CEC59008FC09D /* StoredCashAppPayPaymentMethod.swift */; };
A06B7A9C2A1CEEAE008FC09D /* AnyCashAppPayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A06B7A9B2A1CEEAE008FC09D /* AnyCashAppPayConfiguration.swift */; };
Expand Down Expand Up @@ -1712,6 +1715,7 @@
A00167742BD164A20099A6AE /* CardKCPValidatorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardKCPValidatorsTests.swift; sourceTree = "<group>"; };
A00167782BD274E40099A6AE /* BrazilSocialSecurityNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazilSocialSecurityNumberValidator.swift; sourceTree = "<group>"; };
A001677A2BD278410099A6AE /* BrazilSocialSecurityNumberValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazilSocialSecurityNumberValidatorTests.swift; sourceTree = "<group>"; };
A00580A52CD8CBC300294F2E /* EventAnalyticsProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventAnalyticsProviderTests.swift; sourceTree = "<group>"; };
A009837C279859DC007E68C4 /* ACHDirectDebitComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHDirectDebitComponentTests.swift; sourceTree = "<group>"; };
A009837E27986B50007E68C4 /* BankDetailsEncryptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankDetailsEncryptorTests.swift; sourceTree = "<group>"; };
A00F73A22B3DA38A00E9252B /* DisableStoredPaymentMethodRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableStoredPaymentMethodRequest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1757,6 +1761,8 @@
A04F8C2129E5950100F3F62B /* AdyenCashAppPay.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = AdyenCashAppPay.docc; sourceTree = "<group>"; };
A05A9B7D2B8F8DF300E5E178 /* AnalyticsEventDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventDataSourceTests.swift; sourceTree = "<group>"; };
A05D7211276A28EE0087F3E9 /* DocumentComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentComponentTests.swift; sourceTree = "<group>"; };
A06170A82CCF96B300AF388A /* EventAnalyticsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventAnalyticsProvider.swift; sourceTree = "<group>"; };
A06170AA2CCFE0F600AF388A /* AnalyticsProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsProviderProtocol.swift; sourceTree = "<group>"; };
A0644AB628A5120C00BBF4A9 /* StorePaymentMethodFieldAware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentMethodFieldAware.swift; sourceTree = "<group>"; };
A06B7A992A1CEC59008FC09D /* StoredCashAppPayPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredCashAppPayPaymentMethod.swift; sourceTree = "<group>"; };
A06B7A9B2A1CEEAE008FC09D /* AnyCashAppPayConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCashAppPayConfiguration.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3413,6 +3419,8 @@
children = (
C94632BD27BA6985003DD81F /* AnalyticsProvider.swift */,
C9B6683427C7CB7A006950B9 /* AnalyticsFlavor.swift */,
A06170A82CCF96B300AF388A /* EventAnalyticsProvider.swift */,
A06170AA2CCFE0F600AF388A /* AnalyticsProviderProtocol.swift */,
);
path = AnalyticsProvider;
sourceTree = "<group>";
Expand Down Expand Up @@ -3509,6 +3517,7 @@
C97C16A928059A5A00534419 /* AnalyticsEventTests.swift */,
C97C16AB280702B200534419 /* AnalyticsFlavorTests.swift */,
A05A9B7D2B8F8DF300E5E178 /* AnalyticsEventDataSourceTests.swift */,
A00580A52CD8CBC300294F2E /* EventAnalyticsProviderTests.swift */,
);
path = Analytics;
sourceTree = "<group>";
Expand Down Expand Up @@ -6707,6 +6716,7 @@
B6D808042BCD147600F3F5EB /* UILabelHelpersTests.swift in Sources */,
B62D48B72BBE8DBE001EF01A /* AnalyticsProviderMock.swift in Sources */,
B6D808112BCD147D00F3F5EB /* BankDetailsEncryptorTests.swift in Sources */,
A00580A62CD8CBC300294F2E /* EventAnalyticsProviderTests.swift in Sources */,
B6D808332BCD1F8900F3F5EB /* LogoGeneratorTests.swift in Sources */,
B6D808302BCD1F8900F3F5EB /* DateValidationTests.swift in Sources */,
B6D808442BCD2B0600F3F5EB /* JWAA256CBCHS512AlgorithmTests.swift in Sources */,
Expand Down Expand Up @@ -6989,6 +6999,7 @@
E788A4FB2658034400089448 /* ShopperInformation.swift in Sources */,
E7085B172628B29600D0153B /* FormPhoneExtensionPickerItemView.swift in Sources */,
F926D52C23F59ABE00D058D3 /* QiwiWalletPaymentMethod.swift in Sources */,
A06170AB2CCFE0F600AF388A /* AnalyticsProviderProtocol.swift in Sources */,
A06B7AA72A274A83008FC09D /* ResultHelpers.swift in Sources */,
F97C857725C1D5BA00D7F85C /* UIViewHelpers.swift in Sources */,
E95E24C022C2593900973184 /* ApplePayPaymentMethod.swift in Sources */,
Expand All @@ -7013,6 +7024,7 @@
E7D5311424462D87000046B4 /* FormButtonItemView.swift in Sources */,
813EF9EA2A5FE38700C65D15 /* FormPickerSearchViewController+EmptyView.swift in Sources */,
E7085C81262DD44E00D0153B /* AllCountriesPhoneExtensions.swift in Sources */,
A06170A92CCF96B300AF388A /* EventAnalyticsProvider.swift in Sources */,
00EACBBD2876F9DC0082B360 /* FormAttributedLabelItem.swift in Sources */,
F90E95F227280BD5007E382B /* CoreListDataSource.swift in Sources */,
F926D53323F5A5D000D058D3 /* NumericStringValidator.swift in Sources */,
Expand Down
131 changes: 18 additions & 113 deletions Adyen/Analytics/AnalyticsProvider/AnalyticsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,40 @@
import AdyenNetworking
import Foundation

@_spi(AdyenInternal)
public protocol AnalyticsProviderProtocol {

var checkoutAttemptId: String? { get }

/// Sends the initial data and retrieves the checkout attempt id as a response.
func sendInitialAnalytics(with flavor: AnalyticsFlavor, additionalFields: AdditionalAnalyticsFields?)

/// Adds an info event to be sent.
func add(info: AnalyticsEventInfo)

/// Adds a log event to be sent.
func add(log: AnalyticsEventLog)

/// Adds an error event to be sent.
func add(error: AnalyticsEventError)
}

internal final class AnalyticsProvider: AnalyticsProviderProtocol {

private enum Constants {
static let batchInterval: TimeInterval = 10
static let infoLimit = 50
static let logLimit = 5
static let errorLimit = 5
}
internal final class AnalyticsProvider: AnyAnalyticsProvider {

// MARK: - Properties

internal let apiClient: APIClientProtocol
internal let configuration: AnalyticsConfiguration
internal private(set) var checkoutAttemptId: String?
internal let eventDataSource: AnyAnalyticsEventDataSource
internal var checkoutAttemptId: String? {
didSet {
eventAnalyticsProvider?.checkoutAttemptId = checkoutAttemptId
}
}

internal var eventAnalyticsProvider: AnyEventAnalyticsProvider?

private let uniqueAssetAPIClient: UniqueAssetAPIClient<InitialAnalyticsResponse>

private var batchTimer: Timer?
private let batchInterval: TimeInterval
private let context: AnalyticsContext

// MARK: - Initializers

internal init(
apiClient: APIClientProtocol,
configuration: AnalyticsConfiguration,
eventDataSource: AnyAnalyticsEventDataSource,
batchInterval: TimeInterval = Constants.batchInterval
context: AnalyticsContext,
eventAnalyticsProvider: AnyEventAnalyticsProvider?
) {
self.apiClient = apiClient
self.configuration = configuration
self.context = context
self.eventAnalyticsProvider = eventAnalyticsProvider
self.uniqueAssetAPIClient = UniqueAssetAPIClient<InitialAnalyticsResponse>(apiClient: apiClient)
self.eventDataSource = eventDataSource
self.batchInterval = batchInterval
}

deinit {
// attempt to send remaining events on deallocation
batchTimer?.invalidate()
sendEventsIfNeeded()
}

// MARK: - AnalyticsProviderProtocol
// MARK: - AnyAnalyticsProvider

internal func sendInitialAnalytics(with flavor: AnalyticsFlavor, additionalFields: AdditionalAnalyticsFields?) {
let analyticsData = AnalyticsData(
flavor: flavor,
additionalFields: additionalFields,
context: configuration.context
context: context
)

let initialAnalyticsRequest = InitialAnalyticsRequest(data: analyticsData)
Expand All @@ -84,87 +51,25 @@ internal final class AnalyticsProvider: AnalyticsProviderProtocol {
}

internal func add(info: AnalyticsEventInfo) {
guard configuration.isEnabled else { return }

eventDataSource.add(info: info)
eventAnalyticsProvider?.add(info: info)
}

internal func add(log: AnalyticsEventLog) {
guard configuration.isEnabled else { return }

eventDataSource.add(log: log)
sendEventsIfNeeded()
eventAnalyticsProvider?.add(log: log)
}

internal func add(error: AnalyticsEventError) {
guard configuration.isEnabled else { return }

eventDataSource.add(error: error)
sendEventsIfNeeded()
}

internal func sendEventsIfNeeded() {
guard configuration.isEnabled else { return }
guard let request = requestWithAllEvents() else { return }

apiClient.perform(request) { [weak self] result in
guard let self else { return }
// clear the sent events on successful send
switch result {
case .success:
self.removeEvents(sentBy: request)
self.startNextTimer()
case .failure:
break
}
}
eventAnalyticsProvider?.add(error: error)
}

// MARK: - Private

/// Checks the event arrays safely and creates the request with them if there is any to send.
private func requestWithAllEvents() -> AnalyticsRequest? {
guard let checkoutAttemptId,
let events = eventDataSource.allEvents() else { return nil }

// as per this call's limitation, we only send up to the
// limit of each event and discard the older ones
let platform = configuration.context.platform.rawValue
var request = AnalyticsRequest(
checkoutAttemptId: checkoutAttemptId,
platform: platform
)
request.infos = events.infos.suffix(Constants.infoLimit)
request.logs = events.logs.suffix(Constants.logLimit)
request.errors = events.errors.suffix(Constants.errorLimit)
return request
}

private func saveCheckoutAttemptId(from result: Result<InitialAnalyticsResponse, Error>) {
switch result {
case let .success(response):
checkoutAttemptId = response.checkoutAttemptId
startNextTimer()
case .failure:
checkoutAttemptId = nil
}
}

private func removeEvents(sentBy request: AnalyticsRequest) {
let collection = AnalyticsEventWrapper(
infos: request.infos,
logs: request.logs,
errors: request.errors
)
eventDataSource.removeEvents(matching: collection)
}

private func startNextTimer() {
guard configuration.isEnabled else { return }

batchTimer?.invalidate()
batchTimer = Timer.scheduledTimer(withTimeInterval: batchInterval, repeats: true) { [weak self] _ in
self?.sendEventsIfNeeded()
}
}
}
32 changes: 32 additions & 0 deletions Adyen/Analytics/AnalyticsProvider/AnalyticsProviderProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

@_spi(AdyenInternal)
public protocol AnyInitialAnalyticsProvider {

/// Sends the initial data and retrieves the checkout attempt id as a response.
func sendInitialAnalytics(with flavor: AnalyticsFlavor, additionalFields: AdditionalAnalyticsFields?)
}

@_spi(AdyenInternal)
public protocol AnyEventAnalyticsProvider {

var checkoutAttemptId: String? { get set }

/// Adds an info event to be sent.
func add(info: AnalyticsEventInfo)

/// Adds a log event to be sent.
func add(log: AnalyticsEventLog)

/// Adds an error event to be sent.
func add(error: AnalyticsEventError)
}

@_spi(AdyenInternal)
public protocol AnyAnalyticsProvider: AnyInitialAnalyticsProvider, AnyEventAnalyticsProvider {}
111 changes: 111 additions & 0 deletions Adyen/Analytics/AnalyticsProvider/EventAnalyticsProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import AdyenNetworking
import Foundation

internal final class EventAnalyticsProvider: AnyEventAnalyticsProvider {

private enum Constants {
static let batchInterval: TimeInterval = 10
static let infoLimit = 50
static let logLimit = 5
static let errorLimit = 5
}

internal var checkoutAttemptId: String?
internal let apiClient: APIClientProtocol
internal let eventDataSource: AnyAnalyticsEventDataSource

private let context: AnalyticsContext
private var batchTimer: Timer?
private let batchInterval: TimeInterval

internal init(
apiClient: APIClientProtocol,
context: AnalyticsContext,
eventDataSource: AnyAnalyticsEventDataSource,
batchInterval: TimeInterval = Constants.batchInterval
) {
self.apiClient = apiClient
self.eventDataSource = eventDataSource
self.context = context
self.batchInterval = batchInterval
startNextTimer()
}

deinit {
// attempt to send remaining events on deallocation
batchTimer?.invalidate()
sendEventsIfNeeded()
}

internal func add(info: AnalyticsEventInfo) {
eventDataSource.add(info: info)
}

internal func add(log: AnalyticsEventLog) {
eventDataSource.add(log: log)
sendEventsIfNeeded()
}

internal func add(error: AnalyticsEventError) {
eventDataSource.add(error: error)
sendEventsIfNeeded()
}

internal func sendEventsIfNeeded() {
guard let request = requestWithAllEvents() else { return }

apiClient.perform(request) { [weak self] result in
guard let self else { return }
// clear the sent events on successful send
switch result {
case .success:
self.removeEvents(sentBy: request)
self.startNextTimer()
case .failure:
break
}
}
}

// MARK: - Private

/// Checks the event arrays safely and creates the request with them if there is any to send.
private func requestWithAllEvents() -> AnalyticsRequest? {
guard let checkoutAttemptId,
let events = eventDataSource.allEvents() else { return nil }

// as per this call's limitation, we only send up to the
// limit of each event and discard the older ones
let platform = context.platform.rawValue
var request = AnalyticsRequest(
checkoutAttemptId: checkoutAttemptId,
platform: platform
)
request.infos = events.infos.suffix(Constants.infoLimit)
request.logs = events.logs.suffix(Constants.logLimit)
request.errors = events.errors.suffix(Constants.errorLimit)
return request
}

private func removeEvents(sentBy request: AnalyticsRequest) {
let collection = AnalyticsEventWrapper(
infos: request.infos,
logs: request.logs,
errors: request.errors
)
eventDataSource.removeEvents(matching: collection)
}

private func startNextTimer() {
batchTimer?.invalidate()
batchTimer = Timer.scheduledTimer(withTimeInterval: batchInterval, repeats: true) { [weak self] _ in
self?.sendEventsIfNeeded()
}
}
}
Loading

0 comments on commit 785fd56

Please sign in to comment.