Skip to content

Commit

Permalink
Add analytics for Embedded
Browse files Browse the repository at this point in the history
  • Loading branch information
wooj-stripe committed Oct 17, 2024
1 parent ad08ed6 commit 91a8ee7
Show file tree
Hide file tree
Showing 20 changed files with 223 additions and 136 deletions.
9 changes: 9 additions & 0 deletions StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ import Foundation
case mcInitCustomDefault = "mc_custom_init_default"
case mcInitCompleteDefault = "mc_complete_init_default"

// MARK: - Embedded Payment Element init
case mcInitEmbedded = "mc_embedded_init"

// MARK: - PaymentSheet Show
case mcShowCustomNewPM = "mc_custom_sheet_newpm_show"
case mcShowCustomSavedPM = "mc_custom_sheet_savedpm_show"
Expand Down Expand Up @@ -102,6 +105,9 @@ import Foundation
case mcPaymentCompleteApplePayFailure = "mc_complete_payment_applepay_failure"
case mcPaymentCompleteLinkFailure = "mc_complete_payment_link_failure"

case mcPaymentEmbeddedSuccess = "mc_embedded_payment_success"
case mcPaymentEmbeddedFailure = "mc_embedded_payment_failure"

// MARK: - PaymentSheet Option Selected
case mcOptionSelectCustomNewPM = "mc_custom_paymentoption_newpm_select"
case mcOptionSelectCustomSavedPM = "mc_custom_paymentoption_savedpm_select"
Expand All @@ -111,10 +117,12 @@ import Foundation
case mcOptionSelectCompleteSavedPM = "mc_complete_paymentoption_savedpm_select"
case mcOptionSelectCompleteApplePay = "mc_complete_paymentoption_applepay_select"
case mcOptionSelectCompleteLink = "mc_complete_paymentoption_link_select"
case mcOptionSelectEmbeddedSavedPM = "mc_embedded_paymentoption_savedpm_select"

// MARK: - PaymentSheet Saved Payment Method Removed
case mcOptionRemoveCustomSavedPM = "mc_custom_paymentoption_removed"
case mcOptionRemoveCompleteSavedPM = "mc_complete_paymentoption_removed"
case mcOptionRemoveEmbeddedSavedPM = "mc_embedded_paymentoption_removed"

// MARK: - Link Signup
case linkSignupCheckboxChecked = "link.signup.checkbox_checked"
Expand Down Expand Up @@ -264,6 +272,7 @@ import Foundation
case unexpectedPaymentSheetViewControllerError = "unexpected_error.paymentsheet.paymentsheetviewcontroller"
case unexpectedFlowControllerViewControllerError = "unexpected_error.paymentsheet.flowcontrollerviewcontroller"
case unexpectedPaymentHandlerError = "unexpected_error.paymenthandler"
case unexpectedErrorPaymentSheetAnalytics = "unexpected_error.paymentsheetanalyticshelper"

// MARK: - Misc. errors
case stripePaymentSheetDownloadManagerError = "stripepaymentsheet.downloadmanager.error"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,58 @@ import Foundation

final class PaymentSheetAnalyticsHelper {
let analyticsClient: STPAnalyticsClient
let isCustom: Bool
let configuration: PaymentSheet.Configuration
let integrationShape: IntegrationShape
let configuration: PaymentElementConfiguration

// Vars set later as PaymentSheet successfully loads, etc.
var intent: Intent?
var elementsSession: STPElementsSession?
var loadingStartDate: Date?
private var startTimes: [TimeMeasurement: Date] = [:]

enum IntegrationShape {
case flowController
case complete
case embedded
}

init(
isCustom: Bool,
configuration: PaymentSheet.Configuration,
integrationShape: IntegrationShape,
configuration: PaymentElementConfiguration,
analyticsClient: STPAnalyticsClient = .sharedClient
) {
self.isCustom = isCustom
self.integrationShape = integrationShape
self.configuration = configuration
self.analyticsClient = analyticsClient
}

func logInitialized() {
let event: STPAnalyticEvent = {
switch (configuration.customer != nil, configuration.applePay != nil) {
case (false, false):
return isCustom ? .mcInitCustomDefault : .mcInitCompleteDefault
case (true, false):
return isCustom ? .mcInitCustomCustomer : .mcInitCompleteCustomer
case (false, true):
return isCustom ? .mcInitCustomApplePay : .mcInitCompleteApplePay
case (true, true):
return isCustom ? .mcInitCustomCustomerApplePay : .mcInitCompleteCustomerApplePay
switch integrationShape {
case .flowController:
switch (configuration.customer != nil, configuration.applePay != nil) {
case (false, false):
return .mcInitCustomDefault
case (true, false):
return .mcInitCustomCustomer
case (false, true):
return .mcInitCustomApplePay
case (true, true):
return .mcInitCustomCustomerApplePay
}
case .complete:
switch (configuration.customer != nil, configuration.applePay != nil) {
case (false, false):
return .mcInitCompleteDefault
case (true, false):
return .mcInitCompleteCustomer
case (false, true):
return .mcInitCompleteApplePay
case (true, true):
return .mcInitCompleteCustomerApplePay
}
case .embedded:
return .mcInitEmbedded
}
}()
log(event: event)
Expand Down Expand Up @@ -110,6 +132,11 @@ final class PaymentSheetAnalyticsHelper {
}

func logShow(showingSavedPMList: Bool) {
if case .embedded = integrationShape {
stpAssertionFailure("logShow() is not supported for embedded integration")
return
}
let isCustom = integrationShape == .flowController
if !isCustom {
startTimeMeasurement(.checkout)
}
Expand All @@ -126,7 +153,8 @@ final class PaymentSheetAnalyticsHelper {

func logSavedPMScreenOptionSelected(option: SavedPaymentOptionsViewController.Selection) {
let (event, selectedLPM): (STPAnalyticEvent, String?) = {
if isCustom {
switch integrationShape {
case .flowController:
switch option {
case .add:
return (.mcOptionSelectCustomNewPM, nil)
Expand All @@ -137,7 +165,7 @@ final class PaymentSheetAnalyticsHelper {
case .link:
return (.mcOptionSelectCustomLink, nil)
}
} else {
case .complete:
switch option {
case .add:
return (.mcOptionSelectCompleteNewPM, nil)
Expand All @@ -148,6 +176,13 @@ final class PaymentSheetAnalyticsHelper {
case .link:
return (.mcOptionSelectCompleteLink, nil)
}
case .embedded:
if case .saved(let paymentMethod) = option {
return (.mcOptionSelectEmbeddedSavedPM, paymentMethod.type.identifier)
} else {
stpAssertionFailure("Embedded should only use this function to record tapped saved payment methods")
return (.unexpectedErrorPaymentSheetAnalytics, nil)
}
}
}()
log(event: event, selectedLPM: selectedLPM)
Expand All @@ -157,7 +192,16 @@ final class PaymentSheetAnalyticsHelper {
log(event: .paymentSheetCarouselPaymentMethodTapped, selectedLPM: paymentMethodTypeIdentifier)
}
func logSavedPaymentMethodRemoved(paymentMethod: STPPaymentMethod) {
let event: STPAnalyticEvent = isCustom ? .mcOptionRemoveCustomSavedPM : .mcOptionRemoveCompleteSavedPM
let event: STPAnalyticEvent = {
switch integrationShape {
case .flowController:
return .mcOptionRemoveCustomSavedPM
case .complete:
return .mcOptionRemoveCompleteSavedPM
case .embedded:
return .mcOptionRemoveEmbeddedSavedPM
}
}()
log(event: event, selectedLPM: paymentMethod.type.identifier)
}

Expand Down Expand Up @@ -233,7 +277,8 @@ final class PaymentSheetAnalyticsHelper {
}

let event: STPAnalyticEvent = {
if isCustom {
switch integrationShape {
case .flowController:
switch paymentOption {
case .new, .external:
return success ? .mcPaymentCustomNewPMSuccess : .mcPaymentCustomNewPMFailure
Expand All @@ -244,7 +289,7 @@ final class PaymentSheetAnalyticsHelper {
case .link:
return success ? .mcPaymentCustomLinkSuccess : .mcPaymentCustomLinkFailure
}
} else {
case .complete:
switch paymentOption {
case .new, .external:
return success ? .mcPaymentCompleteNewPMSuccess : .mcPaymentCompleteNewPMFailure
Expand All @@ -255,6 +300,8 @@ final class PaymentSheetAnalyticsHelper {
case .link:
return success ? .mcPaymentCompleteLinkSuccess : .mcPaymentCompleteLinkFailure
}
case .embedded:
return success ? .mcPaymentEmbeddedSuccess : .mcPaymentEmbeddedFailure
}
}()

Expand Down Expand Up @@ -338,6 +385,21 @@ extension STPAnalyticsClient {
extension PaymentSheet.Configuration {
/// Serializes the configuration into a safe dictionary containing no PII for analytics logging
var analyticPayload: [String: Any] {
var payload = commonAnalyticPayload
payload["payment_method_layout"] = paymentMethodLayout.description
return payload
}
}

extension EmbeddedPaymentElement.Configuration {
/// Serializes the configuration into a safe dictionary containing no PII for analytics logging
var analyticPayload: [String: Any] {
return commonAnalyticPayload
}
}

extension PaymentElementConfiguration {
var commonAnalyticPayload: [String: Any] {
var payload = [String: Any]()
payload["allows_delayed_payment_methods"] = allowsDelayedPaymentMethods
payload["apple_pay_config"] = applePay != nil
Expand All @@ -350,9 +412,7 @@ extension PaymentSheet.Configuration {
payload["appearance"] = appearance.analyticPayload
payload["billing_details_collection_configuration"] = billingDetailsCollectionConfiguration.analyticPayload
payload["preferred_networks"] = preferredNetworks?.map({ STPCardBrandUtilities.apiValue(from: $0) }).joined(separator: ", ")
payload["payment_method_layout"] = paymentMethodLayout.description
payload["card_brand_acceptance"] = cardBrandAcceptance != .all

return payload
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class CustomerAddPaymentMethodViewController: UIViewController {
isSettingUp: true,
countryCode: nil,
savePaymentMethodConsentBehavior: savePaymentMethodConsentBehavior,
analyticsHelper: .init(isCustom: false, configuration: PaymentSheet.Configuration.init()) // TODO(MOBILESDK-2548) Just use a dummy analytics helper; we don't look at these analytics.
analyticsHelper: nil
).make()
formElement.delegate = self
return formElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ final class CardSectionElement: ContainerElement {
#endif
}()
let cardSection: SectionElement
let analyticsHelper: PaymentSheetAnalyticsHelper
let analyticsHelper: PaymentSheetAnalyticsHelper?

struct DefaultValues {
internal init(name: String? = nil, pan: String? = nil, cvc: String? = nil, expiry: String? = nil) {
Expand Down Expand Up @@ -68,7 +68,7 @@ final class CardSectionElement: ContainerElement {
cardBrandChoiceEligible: Bool = false,
hostedSurface: HostedSurface,
theme: ElementsAppearance = .default,
analyticsHelper: PaymentSheetAnalyticsHelper,
analyticsHelper: PaymentSheetAnalyticsHelper?,
cardBrandFilter: CardBrandFilter = .default
) {
self.hostedSurface = hostedSurface
Expand Down Expand Up @@ -193,20 +193,20 @@ final class CardSectionElement: ContainerElement {
STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: .paymentSheetCardNumberCompleted)
}
}

// Send an analytic if we are disallowing a card brand
if case .invalid(let error, _) = panElement.validationState,
let specificError = error as? TextFieldElement.PANConfiguration.Error,
case .disallowedBrand(let brand) = specificError,
lastDisallowedCardBrandLogged != brand {

STPAnalyticsClient.sharedClient.logPaymentSheetEvent(
event: .paymentSheetDisallowedCardBrand,
params: ["brand": STPCardBrandUtilities.apiValue(from: brand)]
)
lastDisallowedCardBrandLogged = brand
}

delegate?.didUpdate(element: self)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import UIKit
@objc(STP_Internal_CardSectionWithScannerView)
final class CardSectionWithScannerView: UIView {
let cardSectionView: UIView
let analyticsHelper: PaymentSheetAnalyticsHelper
let analyticsHelper: PaymentSheetAnalyticsHelper?
lazy var cardScanButton: UIButton = {
let button = UIButton.makeCardScanButton(theme: theme)
button.addTarget(self, action: #selector(didTapCardScanButton), for: .touchUpInside)
Expand All @@ -38,7 +38,7 @@ final class CardSectionWithScannerView: UIView {
weak var delegate: CardSectionWithScannerViewDelegate?
private let theme: ElementsAppearance

init(cardSectionView: UIView, delegate: CardSectionWithScannerViewDelegate, theme: ElementsAppearance = .default, analyticsHelper: PaymentSheetAnalyticsHelper) {
init(cardSectionView: UIView, delegate: CardSectionWithScannerViewDelegate, theme: ElementsAppearance = .default, analyticsHelper: PaymentSheetAnalyticsHelper?) {
self.cardSectionView = cardSectionView
self.delegate = delegate
self.theme = theme
Expand All @@ -62,7 +62,7 @@ final class CardSectionWithScannerView: UIView {
}

@objc func didTapCardScanButton() {
analyticsHelper.logFormInteracted(paymentMethodTypeIdentifier: "card")
analyticsHelper?.logFormInteracted(paymentMethodTypeIdentifier: "card")
setCardScanVisible(true)
cardScanningView.start()
becomeFirstResponder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extension EmbeddedPaymentElement {
static func makeView(
configuration: Configuration,
loadResult: PaymentSheetLoader.LoadResult,
analyticsHelper: PaymentSheetAnalyticsHelper,
delegate: EmbeddedPaymentMethodsViewDelegate? = nil
) -> EmbeddedPaymentMethodsView {
let shouldShowApplePay = PaymentSheet.isApplePayEnabled(elementsSession: loadResult.elementsSession, configuration: configuration)
Expand All @@ -36,7 +37,8 @@ extension EmbeddedPaymentElement {
let mandateProvider = VerticalListMandateProvider(
configuration: configuration,
elementsSession: loadResult.elementsSession,
intent: loadResult.intent
intent: loadResult.intent,
analyticsHelper: analyticsHelper
)
return EmbeddedPaymentMethodsView(
initialSelection: initialSelection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ public final class EmbeddedPaymentElement {
intentConfiguration: IntentConfiguration,
configuration: Configuration
) async throws -> EmbeddedPaymentElement {
// TODO(porter) Should we create a new analytics helper specific to embedded? Figured this out when we do analytics.
let analyticsHelper = PaymentSheetAnalyticsHelper(isCustom: true, configuration: PaymentSheet.Configuration())
AnalyticsHelper.shared.generateSessionID()
STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: EmbeddedPaymentElement.self)
let analyticsHelper = PaymentSheetAnalyticsHelper(integrationShape: .embedded, configuration: configuration)

let loadResult = try await PaymentSheetLoader.load(
mode: .deferredIntent(intentConfiguration),
Expand All @@ -75,7 +75,8 @@ public final class EmbeddedPaymentElement {
)
let embeddedPaymentElement: EmbeddedPaymentElement = .init(
configuration: configuration,
loadResult: loadResult
loadResult: loadResult,
analyticsHelper: analyticsHelper
)
return embeddedPaymentElement
}
Expand Down Expand Up @@ -167,21 +168,33 @@ public final class EmbeddedPaymentElement {
internal private(set) var embeddedPaymentMethodsView: EmbeddedPaymentMethodsView
internal private(set) var loadResult: PaymentSheetLoader.LoadResult
internal private(set) var currentUpdateTask: Task<UpdateResult, Never>?
private let analyticsHelper: PaymentSheetAnalyticsHelper

private init(
configuration: Configuration,
loadResult: PaymentSheetLoader.LoadResult
loadResult: PaymentSheetLoader.LoadResult,
analyticsHelper: PaymentSheetAnalyticsHelper,
delegate: EmbeddedPaymentElementDelegate? = nil
) {
self.configuration = configuration
self.loadResult = loadResult
self.embeddedPaymentMethodsView = Self.makeView(
configuration: configuration,
loadResult: loadResult
loadResult: loadResult,
analyticsHelper: analyticsHelper
)
self.analyticsHelper = analyticsHelper
analyticsHelper.logInitialized()
self.embeddedPaymentMethodsView.delegate = self
}
}

// MARK: - STPAnalyticsProtocol
/// :nodoc:
@_spi(STP) extension EmbeddedPaymentElement: STPAnalyticsProtocol {
@_spi(STP) public static let stp_analyticsIdentifier: String = "EmbeddedPaymentElement"
}

// MARK: - Completion-block based APIs
extension EmbeddedPaymentElement {
/// Creates an instance of `EmbeddedPaymentElement`
Expand Down
Loading

0 comments on commit 91a8ee7

Please sign in to comment.