Skip to content

Commit

Permalink
chore New analytics initial call (#1497)
Browse files Browse the repository at this point in the history
New analytics replacing the existing checkout attempt Id fetch and
initial telemetry with a single call.

The new analytics is on a new webapp so it requires a new `baseURL`
which needs a new environment, thus `AnalayticsEnvironment` is added.

- A new api context with this environment (`analyticsApiContext`) is
created internally during the original context's creation.

This new call combines sending the analytics data and returns the
checkout attempt id in the response. So this call needs to be done only
once per payment flow. So the call is made on component load unless the
component is inside dropIn in which case dropIn will have made the call
already.
  • Loading branch information
erenbesel authored Feb 12, 2024
2 parents bc6a3dd + ad4147d commit c3f0429
Show file tree
Hide file tree
Showing 70 changed files with 765 additions and 665 deletions.
76 changes: 48 additions & 28 deletions Adyen.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Adyen/Analytics/AnalyticsProvider/AnalyticsFlavor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Copyright (c) 2023 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 enum AnalyticsFlavor {
case components(type: PaymentMethodType)
case dropIn(type: String = "dropin", paymentMethods: [String])

public var value: String {
switch self {
case .components:
return "components"
case .dropIn:
return "dropin"
}
}
}
60 changes: 32 additions & 28 deletions Adyen/Analytics/AnalyticsProvider/AnalyticsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@ public struct AnalyticsConfiguration {

/// A Boolean value that determines whether analytics is enabled.
public var isEnabled = true

@_spi(AdyenInternal)
public var isTelemetryEnabled = true

@_spi(AdyenInternal)
public var context: TelemetryContext = .init()
public var context: AnalyticsContext = .init()

// MARK: - Initializers

Expand All @@ -28,20 +25,26 @@ public struct AnalyticsConfiguration {
}

@_spi(AdyenInternal)
/// Additional fields to be provided with a ``TelemetryRequest``
/// Additional fields to be provided with an ``InitialAnalyticsRequest``
public struct AdditionalAnalyticsFields {
/// The amount of the payment
public let amount: Amount?

public let sessionId: String?

public init(amount: Amount?, sessionId: String?) {
self.amount = amount
self.sessionId = sessionId
}
}

@_spi(AdyenInternal)
public protocol AnalyticsProviderProtocol: TelemetryTrackerProtocol {

var checkoutAttemptId: String? { get }
public protocol AnalyticsProviderProtocol {

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

var additionalFields: (() -> AdditionalAnalyticsFields)? { get }
var checkoutAttemptId: String? { get }
}

internal final class AnalyticsProvider: AnalyticsProviderProtocol {
Expand All @@ -51,8 +54,7 @@ internal final class AnalyticsProvider: AnalyticsProviderProtocol {
internal let apiClient: APIClientProtocol
internal let configuration: AnalyticsConfiguration
internal private(set) var checkoutAttemptId: String?
internal var additionalFields: (() -> AdditionalAnalyticsFields)?
private let uniqueAssetAPIClient: UniqueAssetAPIClient<CheckoutAttemptIdResponse>
private let uniqueAssetAPIClient: UniqueAssetAPIClient<InitialAnalyticsResponse>

// MARK: - Initializers

Expand All @@ -62,32 +64,34 @@ internal final class AnalyticsProvider: AnalyticsProviderProtocol {
) {
self.apiClient = apiClient
self.configuration = configuration
self.uniqueAssetAPIClient = UniqueAssetAPIClient<CheckoutAttemptIdResponse>(apiClient: apiClient)
self.uniqueAssetAPIClient = UniqueAssetAPIClient<InitialAnalyticsResponse>(apiClient: apiClient)
}

// MARK: - Internal

internal func fetchAndCacheCheckoutAttemptIdIfNeeded() {
fetchCheckoutAttemptId { _ in /* Do nothing, the point is to trigger the fetching and cache the value */ }
}

internal func fetchCheckoutAttemptId(completion: @escaping (String?) -> Void) {
internal func sendInitialAnalytics(with flavor: AnalyticsFlavor, additionalFields: AdditionalAnalyticsFields?) {
guard configuration.isEnabled else {
checkoutAttemptId = "do-not-track"
completion(checkoutAttemptId)
return
}

let analyticsData = AnalyticsData(flavor: flavor,
additionalFields: additionalFields,
context: configuration.context)

let checkoutAttemptIdRequest = CheckoutAttemptIdRequest()
let initialAnalyticsRequest = InitialAnalyticsRequest(data: analyticsData)

uniqueAssetAPIClient.perform(checkoutAttemptIdRequest) { [weak self] result in
switch result {
case let .success(response):
self?.checkoutAttemptId = response.identifier
completion(response.identifier)
case .failure:
completion(nil)
}
uniqueAssetAPIClient.perform(initialAnalyticsRequest) { [weak self] result in
self?.saveCheckoutAttemptId(from: result)
}
}

private func saveCheckoutAttemptId(from result: Result<InitialAnalyticsResponse, Error>) {
switch result {
case let .success(response):
checkoutAttemptId = response.checkoutAttemptId
case .failure:
checkoutAttemptId = nil
}
}
}
60 changes: 0 additions & 60 deletions Adyen/Analytics/AnalyticsProvider/TelemetryTracker.swift

This file was deleted.

33 changes: 33 additions & 0 deletions Adyen/Analytics/Models/AdyenAnalytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Copyright (c) 2023 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)
/// Used as a singleton to update the sessionId
public final class AnalyticsForSession {

/// Needed to be able to determine if using session
public static var sessionId: String?

private init() { /* Private empty init */ }
}

@_spi(AdyenInternal)
/// A protocol that defines the events that can occur under Checkout Analytics.
public protocol AnalyticsEvent: Encodable {
var timestamp: TimeInterval { get }

var component: String { get }
}

@_spi(AdyenInternal)
public extension AnalyticsEvent {

var timestamp: TimeInterval {
Date().timeIntervalSince1970
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import UIKit
///
/// Used to e.g. override the version + platform from within the Flutter SDK
@_spi(AdyenInternal)
public struct TelemetryContext {
public struct AnalyticsContext {

internal let version: String
internal let platform: Platform
Expand All @@ -26,7 +26,7 @@ public struct TelemetryContext {
}

@_spi(AdyenInternal)
public extension TelemetryContext {
public extension AnalyticsContext {

enum Platform: String {
case iOS = "ios"
Expand All @@ -35,7 +35,7 @@ public extension TelemetryContext {
}
}

internal struct TelemetryData: Encodable {
internal struct AnalyticsData: Encodable {

// MARK: - Properties

Expand Down Expand Up @@ -64,6 +64,8 @@ internal struct TelemetryData: Encodable {
return identifier + String(UnicodeScalar(UInt8(value)))
}
}()

internal let deviceModel = UIDevice.current.model

internal let systemVersion = UIDevice.current.systemVersion

Expand All @@ -79,19 +81,20 @@ internal struct TelemetryData: Encodable {

internal var amount: Amount?

internal var sessionId: String?

internal var paymentMethods: [String] = []

internal let component: String

// MARK: - Initializers

internal init(
flavor: TelemetryFlavor,
amount: Amount?,
context: TelemetryContext
) {
internal init(flavor: AnalyticsFlavor,
additionalFields: AdditionalAnalyticsFields?,
context: AnalyticsContext) {
self.flavor = flavor.value
self.amount = amount
self.amount = additionalFields?.amount
self.sessionId = additionalFields?.sessionId

self.version = context.version
self.platform = context.platform.rawValue
Expand All @@ -102,8 +105,6 @@ internal struct TelemetryData: Encodable {
self.component = type
case let .components(type):
self.component = type.rawValue
default:
self.component = ""
}
}
}
30 changes: 30 additions & 0 deletions Adyen/Analytics/Models/AnalyticsEventError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Copyright (c) 2023 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)
/// Represents an error in the analytics scheme that indicates the flow was interrupted due to an error in the SDK.
public struct AnalyticsEventError: AnalyticsEvent {

public var component: String

public var type: ErrorType

public var code: String?

public var message: String?

public enum ErrorType: String, Encodable {
case network = "Network"
case implementation = "Implementation"
case `internal` = "Internal"
case api = "ApiError"
case sdk = "SdkError"
case thirdParty = "ThirdParty"
case generic = "Generic"
}
}
34 changes: 34 additions & 0 deletions Adyen/Analytics/Models/AnalyticsEventInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Copyright (c) 2023 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)
/// Represents an info event in the analytics scheme that can occur
/// multiple times during the checkout flow, such as input field focus/unfocus etc.
public struct AnalyticsEventInfo: AnalyticsEvent {
public var component: String

public var type: InfoType

public var target: String?

public var isStoredPaymentMethod: Bool?

public var brand: String?

public var validationErrorCode: String?

public var validationErrorMessage: String?

public enum InfoType: String, Encodable {
case selected = "Selected"
case focus = "Focus"
case unfocus = "Unfocus"
case validationError = "ValidationError"
case rendered = "Rendered"
}
}
Loading

0 comments on commit c3f0429

Please sign in to comment.