Skip to content

Commit

Permalink
[Woo POS] MVP analytics: Track remaining MVP events and properties (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
iamgabrielma authored Feb 19, 2025
2 parents 7923e8e + 1bafc53 commit f5ccde0
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 27 deletions.
9 changes: 7 additions & 2 deletions WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1276,9 +1276,10 @@ enum WooAnalyticsStat: String {
case pointOfSaleItemRemovedFromCart = "item_removed_from_cart"
case pointOfSaleCheckoutTapped = "checkout_tapped"
case pointOfSaleBackToCartTapped = "back_to_cart_tapped"
case pointOfSaleBackToCheckoutFromCashTapped = "back_to_checkout_from_cash"
case pointOfSaleClearCartTapped = "clear_cart_tapped"
case pointOfSaleExitMenuItemTapped = "exit_pos_menu_item_tapped"
case pointOfSaleExitConfirmed = "exit_pos_confirmed"
case pointOfSaleExitMenuItemTapped = "exit_menu_item_tapped"
case pointOfSaleExitConfirmed = "exit_confirmed"
case pointOfSaleGetSupportTapped = "get_support_tapped"
case pointOfSaleSimpleProductsExplanationDialogShown = "simple_products_explanation_dialog_shown"
case pointOfSaleCreateNewOrderTapped = "create_new_order_tapped"
Expand All @@ -1287,6 +1288,10 @@ enum WooAnalyticsStat: String {
case pointOfSalePaymentsOnboardingShown = "payments_onboarding_shown"
case pointOfSalePaymentsOnboardingDismissed = "payments_onboarding_dismissed"
case pointOfSaleCardReaderConnectionTapped = "card_reader_connection_tapped"
case pointOfSaleInteractionWithCustomerStarted = "interaction_with_customer_started"
case pointOfSaleViewDocsTapped = "view_docs_tapped"
case pointOfSaleReaderReadyForCardPayment = "reader_ready_for_card_payment"
case pointOfSaleCashCollectPaymentSuccess = "cash_collect_payment_success"

// MARK: Custom Fields events
case productDetailCustomFieldsTapped = "product_detail_custom_fields_tapped"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
var connectedReaderModel: String?

private var customerInteractionStarted: Double = 0
private var orderCreated: Double = 0
private var orderSync: Double = 0
private var cardReaderReady: Double = 0
private var cardReaderTapped: Double = 0
private var checkoutTapCount: Int = 0
Expand All @@ -20,12 +20,12 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func preflightResultReceived(_ result: CardReaderPreflightResult?) { }
func trackProcessingCompletion(intent: Yosemite.PaymentIntent) { }

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
// Property: milliseconds_since_customer_interaction_started
let elapsedTimeSinceCustomerInteraction = calculateElapsedTimeInMilliseconds(since: customerInteractionStarted)

// Property: milliseconds_since_order_creation_success
let elapsedTimeSinceOrderCreation = calculateElapsedTimeInMilliseconds(since: orderCreated)
// Property: milliseconds_since_order_sync_success
let elapsedTimeSinceOrderSync = calculateElapsedTimeInMilliseconds(since: orderSync)

// Property: milliseconds_since_reader_ready_to_collect_payment
let elapsedTimeSinceCardReaderReady = calculateElapsedTimeInMilliseconds(since: cardReaderReady)
Expand All @@ -35,7 +35,7 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin

analytics.track(event: .PointOfSale.cardPresentCollectPaymentSuccess(
millisecondsSinceCustomerIteractionStarted: elapsedTimeSinceCustomerInteraction,
millisecondsSinceOrderCreationSuccess: elapsedTimeSinceOrderCreation,
millisecondsSinceOrderSyncSuccess: elapsedTimeSinceOrderSync,
millisecondsSinceReaderReadyToCollect: elapsedTimeSinceCardReaderReady,
millisecondsSinceCardTapped: elapsedTimeSinceCardTapped,
checkoutTapCount: checkoutTapCount
Expand All @@ -45,6 +45,15 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
resetProcessingPaymentTracking()
}

func trackSuccessfulCashPayment() {
let elapsedTimeSinceCustomerInteraction = calculateElapsedTimeInMilliseconds(since: customerInteractionStarted)

analytics.track(event: .PointOfSale.cashCollectPaymentSuccess(
millisecondsSinceCustomerIteractionStarted: elapsedTimeSinceCustomerInteraction
))
resetCheckoutTapCountTracker()
}

func trackPaymentFailure(with error: any Error) { }
func trackPaymentCancelation(cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) { }
func trackEmailTapped() { }
Expand All @@ -56,15 +65,19 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func trackCustomerInteractionStarted() {
// Any action that is considered as user starting an iteraction resets any ongoing counter
resetAllCountersOnInteractionStarted()
analytics.track(.pointOfSaleInteractionWithCustomerStarted)
customerInteractionStarted = Date().timeIntervalSince1970
}

func trackOrderCreationSuccess() {
orderCreated = trackCurrentTime()
func trackOrderSyncSuccess() {
orderSync = trackCurrentTime()
}

func trackCardReaderReady() {
cardReaderReady = trackCurrentTime()

// As a side effect of knowing when the reader is ready, we track the elapsed from order sync (created or updated)
trackElapsedTimeFromOrderSyncToCardReady()
}

// The Stripe SDK returns multiple `.processing` events, but we want to capture the first one in the stream only.
Expand All @@ -83,6 +96,11 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func resetCheckoutTapCountTracker() {
checkoutTapCount = 0
}

private func trackElapsedTimeFromOrderSyncToCardReady() {
let elapsedTime = cardReaderReady - orderSync
analytics.track(event: .PointOfSale.cardReaderReadyForCardPayment(waitingTime: elapsedTime))
}
}

// Helpers
Expand All @@ -101,7 +119,7 @@ private extension POSCollectOrderPaymentAnalytics {
}

private func resetAllCountersOnInteractionStarted() {
orderCreated = 0
orderSync = 0
cardReaderReady = 0
cardReaderTapped = 0
resetCheckoutTapCountTracker()
Expand All @@ -113,9 +131,10 @@ private extension POSCollectOrderPaymentAnalytics {
// https://github.com/woocommerce/woocommerce-ios/issues/15149
extension CollectOrderPaymentAnalytics {
func trackCustomerInteractionStarted() { }
func trackOrderCreationSuccess() { }
func trackOrderSyncSuccess() { }
func trackCardReaderReady() { }
func trackCardReaderTapped() { }
func trackCheckoutTapped() { }
func resetCheckoutTapCountTracker() { }
func trackSuccessfulCashPayment() { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ extension WooAnalyticsEvent {
static let itemType = "product_type"
static let itemsInCart = "items_in_cart"
static let millisecondsSinceCustomerInteractionStarted = "milliseconds_since_customer_interaction_started"
static let millisecondsSinceOrderCreationSuccess = "milliseconds_since_order_creation_success"
static let millisecondsSinceOrderSyncSuccess = "milliseconds_since_order_sync_success"
static let millisecondsSinceReaderReadyToCollect = "milliseconds_since_reader_ready_to_collect_payment"
static let millisecondsSinceCardTapped = "milliseconds_since_card_tapped"
static let checkoutTapCount = "checkout_tap_count"
static let waitingTime = "waiting_time"
}

static func paymentsOnboardingShown() -> WooAnalyticsEvent {
Expand All @@ -37,19 +38,32 @@ extension WooAnalyticsEvent {
properties: [Key.itemsInCart: itemsInCart])
}

/// Tracks the time elapsed preparing reader for payment, after successful order creation
/// - Parameter waitingTime: Elapsed time from Order creation to card ready for payment
///
static func cardReaderReadyForCardPayment(waitingTime: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleReaderReadyForCardPayment, properties: [Key.waitingTime: "\(waitingTime)"])
}

static func cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double,
millisecondsSinceOrderCreationSuccess: Double,
millisecondsSinceOrderSyncSuccess: Double,
millisecondsSinceReaderReadyToCollect: Double,
millisecondsSinceCardTapped: Double,
checkoutTapCount: Int) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .collectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
Key.millisecondsSinceOrderCreationSuccess: "\(millisecondsSinceOrderCreationSuccess)",
Key.millisecondsSinceOrderSyncSuccess: "\(millisecondsSinceOrderSyncSuccess)",
Key.millisecondsSinceReaderReadyToCollect: "\(millisecondsSinceReaderReadyToCollect)",
Key.millisecondsSinceCardTapped: "\(millisecondsSinceCardTapped)",
Key.checkoutTapCount: "\(checkoutTapCount)"
])
}

static func cashCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleCashCollectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
])
}
}
}

Expand Down
24 changes: 19 additions & 5 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ private extension PointOfSaleAggregateModel {
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
}
}

// Tracks when the order is created or updated successfully
// pdfdoF-6hn#comment-7625-p2
func trackOrderSyncState(_ result: Result<SyncOrderState, Error>) {
switch result {
case .success(let syncState):
switch syncState {
case .newOrder, .orderUpdated:
collectOrderPaymentAnalyticsTracker.trackOrderSyncSuccess()
default:
break
}
case .failure:
break
}
}
}

// MARK: - Card payments
Expand Down Expand Up @@ -242,6 +258,7 @@ extension PointOfSaleAggregateModel {

@MainActor
func cancelCashPayment() async {
analytics.track(.pointOfSaleBackToCheckoutFromCashTapped)
paymentState = .card(.idle)
if case .connected = cardReaderConnectionStatus {
await collectCardPayment()
Expand All @@ -250,8 +267,7 @@ extension PointOfSaleAggregateModel {

private func cashPaymentSuccess() {
paymentState = .cash(.paymentSuccess)
// TODO: Move to trackSuccessfulCashPayment() on #15151
collectOrderPaymentAnalyticsTracker.resetCheckoutTapCountTracker()
collectOrderPaymentAnalyticsTracker.trackSuccessfulCashPayment()
}

@MainActor
Expand Down Expand Up @@ -453,9 +469,7 @@ extension PointOfSaleAggregateModel {
let syncOrderResult = await orderController.syncOrder(for: cart, retryHandler: { [weak self] in
await self?.checkOut()
})
if case .success(.newOrder) = syncOrderResult {
collectOrderPaymentAnalyticsTracker.trackOrderCreationSuccess()
}
trackOrderSyncState(syncOrderResult)
await startPaymentWhenCardReaderConnected()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct POSFloatingControlView: View {
}
Button {
showDocumentation = true
ServiceLocator.analytics.track(.pointOfSaleViewDocsTapped)
} label: {
Label(
title: { Text(Localization.viewDocumentation) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ protocol CollectOrderPaymentAnalyticsTracking {

func trackProcessingCompletion(intent: PaymentIntent)

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData)
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData)

func trackPaymentFailure(with error: Error)

Expand All @@ -26,11 +26,12 @@ protocol CollectOrderPaymentAnalyticsTracking {
func trackReceiptPrintFailed(error: Error)

func trackCustomerInteractionStarted()
func trackOrderCreationSuccess()
func trackOrderSyncSuccess()
func trackCardReaderReady()
func trackCardReaderTapped()
func trackCheckoutTapped()
func resetCheckoutTapCountTracker()
func trackSuccessfulCashPayment()
}

final class CollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
Expand Down Expand Up @@ -89,7 +90,7 @@ final class CollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
}
}

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
analytics.track(event: WooAnalyticsEvent.InPersonPayments
.collectPaymentSuccess(forGatewayID: paymentGatewayAccount?.gatewayID,
countryCode: configuration.countryCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private extension CollectOrderPaymentUseCase {
/// Tracks the successful payments
///
func handleSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
analyticsTracker.trackSuccessfulPayment(capturedPaymentData: capturedPaymentData)
analyticsTracker.trackSuccessfulCardPayment(capturedPaymentData: capturedPaymentData)
}

func handlePaymentCancellation(from cancellationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic

var didCallTrackSuccessfulPayment = false
var spyTrackSuccessfulPaymentCapturedPaymentData: CardPresentCapturedPaymentData? = nil
func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
didCallTrackSuccessfulPayment = true
spyTrackSuccessfulPaymentCapturedPaymentData = capturedPaymentData
}
Expand Down Expand Up @@ -58,7 +58,7 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic
// no-op
}

func trackOrderCreationSuccess() {
func trackOrderSyncSuccess() {
// no-op
}

Expand All @@ -78,4 +78,8 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic
func resetCheckoutTapCountTracker() {
// no-op
}

func trackSuccessfulCashPayment() {
// no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ struct POSCollectOrderPaymentAnalyticsTests {
let capturedPaymentData = CardPresentCapturedPaymentData(paymentMethod: .cardPresent(details: .fake()), receiptParameters: nil)
let expectedEvent = "card_present_collect_payment_success"
let expectedProperties = [
"milliseconds_since_order_creation_success",
"milliseconds_since_order_sync_success",
"milliseconds_since_reader_ready_to_collect_payment",
"milliseconds_since_card_tapped",
"milliseconds_since_customer_interaction_started",
"checkout_tap_count"
]

// When
sut.trackSuccessfulPayment(capturedPaymentData: capturedPaymentData)
sut.trackSuccessfulCardPayment(capturedPaymentData: capturedPaymentData)

// Then
#expect(analyticsProvider.receivedEvents.first(where: { $0 == expectedEvent }) != nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,21 @@ struct PointOfSaleAggregateModelTests {
// Then
#expect(analyticsTracker.didCallTrackCheckoutTapped == true)
}

@available(iOS 17.0, *)
@Test func cancelCashPayment_when_invoked_then_tracks_expected_event() async throws {
// Given
let analyticsTracker = MockCollectOrderPaymentAnalyticsTracker()
let sut = PointOfSaleAggregateModel(itemsController: MockPointOfSaleItemsController(),
cardPresentPaymentService: MockCardPresentPaymentService(),
orderController: MockPointOfSaleOrderController(),
collectOrderPaymentAnalyticsTracker: analyticsTracker)
// When
await sut.cancelCashPayment()

// Then
#expect(analyticsProvider.receivedEvents.first(where: { $0 == "back_to_checkout_from_cash" }) != nil)
}
}
}

Expand Down

0 comments on commit f5ccde0

Please sign in to comment.