From a5ec1163b3173e9c9064b9ba176b39dbff46219b Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:52:27 +0300 Subject: [PATCH 1/6] Track addItemToCart event for scanned items - Expand source_type with scanner - Expand item_type with loading and error types - Add new error field Track loading, error, and success scanner states --- .../WooAnalyticsEvent+PointOfSale.swift | 18 ++++++++++-- .../POS/Models/Cart+BarcodeScanError.swift | 12 +++++--- WooCommerce/Classes/POS/Models/Cart.swift | 11 +++++--- .../Models/PointOfSaleAggregateModel.swift | 28 +++++++++++++++++-- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index a448daa6ab6..557754ae66c 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -25,6 +25,7 @@ extension WooAnalyticsEvent { static let resultsCount = "results_count" static let millisecondsSinceRequestSent = "milliseconds_since_request_sent" static let totalItems = "total_items" + static let error = "error" } static func paymentsOnboardingShown() -> WooAnalyticsEvent { @@ -37,21 +38,29 @@ extension WooAnalyticsEvent { } static func addItemToCart( - sourceView: WooAnalyticsEvent.PointOfSale.SourceView, + sourceView: WooAnalyticsEvent.PointOfSale.SourceView? = nil, sourceViewType: WooAnalyticsEvent.PointOfSale.SourceViewType, itemType: WooAnalyticsEvent.PointOfSale.ItemType, - productType: WooAnalyticsEvent.PointOfSale.CartItemProductType? = nil + productType: WooAnalyticsEvent.PointOfSale.CartItemProductType? = nil, + error: String? = nil ) -> WooAnalyticsEvent { var properties: [String: String] = [ - Key.sourceView: sourceView.rawValue, Key.sourceViewType: sourceViewType.rawValue, Key.itemType: itemType.rawValue ] + if let sourceView { + properties[Key.sourceView] = sourceView.rawValue + } + if let productType { properties[Key.productType] = productType.rawValue } + if let error { + properties[Key.error] = error + } + return WooAnalyticsEvent( statName: .pointOfSaleAddItemToCart, properties: properties @@ -217,6 +226,7 @@ extension WooAnalyticsEvent.PointOfSale { case list case search case preSearch = "pre_search" + case scanner init(isSearching: Bool, searchTerm: String = "") { switch (isSearching, searchTerm.isEmpty) { @@ -235,6 +245,8 @@ extension WooAnalyticsEvent.PointOfSale { enum ItemType: String { case product case coupon + case loading + case error } /// Types of products supported in the POS diff --git a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift index fb47d8c2752..e7f84ebbc0b 100644 --- a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift +++ b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift @@ -30,7 +30,13 @@ extension Cart { } private func subtitle(for error: PointOfSaleBarcodeScanError) -> String { - switch error { + return error.localizedDescription + } +} + +extension PointOfSaleBarcodeScanError { + var localizedDescription: String { + switch self { case .notFound, .unknown: return Localization.notFound case .downloadableProduct, .unsupportedProductType: @@ -45,10 +51,8 @@ extension Cart { } } } -} -private extension Cart { - enum Localization { + private enum Localization { static let notFound = NSLocalizedString( "pointOfSale.barcodeScan.error.notFound", value: "Unknown scanned item", diff --git a/WooCommerce/Classes/POS/Models/Cart.swift b/WooCommerce/Classes/POS/Models/Cart.swift index 9ee582e6975..cce8a2df833 100644 --- a/WooCommerce/Classes/POS/Models/Cart.swift +++ b/WooCommerce/Classes/POS/Models/Cart.swift @@ -100,20 +100,23 @@ extension Cart { } } - mutating func addLoadingItem() -> UUID { + mutating func addLoadingItem() -> Cart.PurchasableItem { let id = UUID() let loadingItem = PurchasableItem.loading(id: id) purchasableItems.insert(loadingItem, at: purchasableItems.startIndex) - return id + return loadingItem } - mutating func updateLoadingItem(id: UUID, with posItem: POSItem) { - guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return } + @discardableResult + mutating func updateLoadingItem(id: UUID, with posItem: POSItem) -> Cart.PurchasableItem? { + guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return nil } if let productItem = createPurchasableItem(id: id, from: posItem) { purchasableItems[index] = productItem + return productItem } else { purchasableItems.remove(at: index) + return nil } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 82b1651188a..9702416b464 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -187,14 +187,38 @@ extension PointOfSaleAggregateModel { extension PointOfSaleAggregateModel { func barcodeScanned(_ barcode: String) { Task { - let placeholderItemID = cart.addLoadingItem() + let placeholderItemID = cart.addLoadingItem().id + + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .loading + ) + ) + do throws(PointOfSaleBarcodeScanError) { let item = try await barcodeScanService.getItem(barcode: barcode) - cart.updateLoadingItem(id: placeholderItemID, with: item) + if let cartItem = cart.updateLoadingItem(id: placeholderItemID, with: item) { + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .product, + productType: .init(cartItem: cartItem) + ) + ) + } } catch { DDLogInfo("Failed to find item by barcode: \(error)") cart.updateLoadingItem(id: placeholderItemID, with: error) await soundPlayer.playSound(.barcodeScanFailure) + + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .error, + error: error.localizedDescription + ) + ) } } } From 0e78b155b44b22a5aef5900ac694fcbc04ae95c3 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:10:04 +0300 Subject: [PATCH 2/6] Only track an error with item wasn't removed from cart --- .../POS/Models/Cart+BarcodeScanError.swift | 7 +++++-- .../Models/PointOfSaleAggregateModel.swift | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift index e7f84ebbc0b..e3adb93b7dd 100644 --- a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift +++ b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift @@ -2,8 +2,9 @@ import Foundation import enum Yosemite.PointOfSaleBarcodeScanError extension Cart { - mutating func updateLoadingItem(id: UUID, with error: PointOfSaleBarcodeScanError) { - guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return } + @discardableResult + mutating func updateLoadingItem(id: UUID, with error: PointOfSaleBarcodeScanError) -> Cart.PurchasableItem? { + guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return nil } purchasableItems[index] = Cart.PurchasableItem( id: id, @@ -12,6 +13,8 @@ extension Cart { quantity: 1, state: .error ) + + return purchasableItems[index] } private func title(for error: PointOfSaleBarcodeScanError) -> String { diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 9702416b464..739b46c7903 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -209,16 +209,18 @@ extension PointOfSaleAggregateModel { } } catch { DDLogInfo("Failed to find item by barcode: \(error)") - cart.updateLoadingItem(id: placeholderItemID, with: error) - await soundPlayer.playSound(.barcodeScanFailure) - - analytics.track( - event: .PointOfSale.addItemToCart( - sourceViewType: .scanner, - itemType: .error, - error: error.localizedDescription + if let _ = cart.updateLoadingItem(id: placeholderItemID, with: error) { + // Only play a sound and track analytics if the item still exists in the cart. + await soundPlayer.playSound(.barcodeScanFailure) + + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .error, + error: error.localizedDescription + ) ) - ) + } } } } From 50b9b10d43a762975f08a388d0dc77152d6b53d3 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:10:40 +0300 Subject: [PATCH 3/6] Track when loading item is removed from cart --- WooCommerce/Classes/POS/Presentation/CartView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index ec72321a1f7..fdcbcc2adc8 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -80,6 +80,12 @@ struct CartView: View { posModel.remove(cartItem: cartItem) } : nil, onCancelLoading: { + ServiceLocator.analytics.track( + event: .PointOfSale.itemRemovedFromCart( + sourceView: .cart, + itemType: .loading + ) + ) posModel.cancelLoadingItem(id: cartItem.id) }) .id(cartItem.id) From 3a3968b5e220bc4e8f82a26d6e3c1ba0a84ff355 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:22:31 +0300 Subject: [PATCH 4/6] Track correct itemType when a cart row is removed --- .../POS/Analytics/WooAnalyticsEvent+PointOfSale.swift | 11 +++++++++++ WooCommerce/Classes/POS/Presentation/CartView.swift | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index 557754ae66c..76a550b3ae9 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -247,6 +247,17 @@ extension WooAnalyticsEvent.PointOfSale { case coupon case loading case error + + init(cartItem: Cart.PurchasableItem) { + switch cartItem.state { + case .loaded: + self = .product + case .loading: + self = .loading + case .error: + self = .error + } + } } /// Types of products supported in the POS diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index fdcbcc2adc8..76140a677a0 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -73,7 +73,7 @@ struct CartView: View { ServiceLocator.analytics.track( event: .PointOfSale.itemRemovedFromCart( sourceView: .cart, - itemType: .product, + itemType: .init(cartItem: cartItem), productType: .init(cartItem: cartItem) ) ) From 58ba45110e093047fad66c873f0452b765731801 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:50:18 +0300 Subject: [PATCH 5/6] Track barcode scan error with a full error object --- .../POS/Analytics/WooAnalyticsEvent+PointOfSale.swift | 10 +++------- .../Classes/POS/Models/PointOfSaleAggregateModel.swift | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index 76a550b3ae9..d29894d287f 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -25,7 +25,6 @@ extension WooAnalyticsEvent { static let resultsCount = "results_count" static let millisecondsSinceRequestSent = "milliseconds_since_request_sent" static let totalItems = "total_items" - static let error = "error" } static func paymentsOnboardingShown() -> WooAnalyticsEvent { @@ -42,7 +41,7 @@ extension WooAnalyticsEvent { sourceViewType: WooAnalyticsEvent.PointOfSale.SourceViewType, itemType: WooAnalyticsEvent.PointOfSale.ItemType, productType: WooAnalyticsEvent.PointOfSale.CartItemProductType? = nil, - error: String? = nil + error: Error? = nil ) -> WooAnalyticsEvent { var properties: [String: String] = [ Key.sourceViewType: sourceViewType.rawValue, @@ -57,13 +56,10 @@ extension WooAnalyticsEvent { properties[Key.productType] = productType.rawValue } - if let error { - properties[Key.error] = error - } - return WooAnalyticsEvent( statName: .pointOfSaleAddItemToCart, - properties: properties + properties: properties, + error: error ) } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 739b46c7903..256be5964c9 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -217,7 +217,7 @@ extension PointOfSaleAggregateModel { event: .PointOfSale.addItemToCart( sourceViewType: .scanner, itemType: .error, - error: error.localizedDescription + error: error ) ) } From cbc63cd62e53f58535144773d69e4d33caa3875b Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:41:50 +0300 Subject: [PATCH 6/6] Update PointOfSaleAggregateModelTests.swift --- .../POS/Models/PointOfSaleAggregateModelTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 6f329e5b6fd..7fa5f46bd0f 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -125,12 +125,12 @@ struct PointOfSaleAggregateModelTests { try #require(cart.purchasableItems.isEmpty) // When - let id = cart.addLoadingItem() + let loadingItem = cart.addLoadingItem() // Then #expect(cart.purchasableItems.count == 1) let item = try #require(cart.purchasableItems.first) - #expect(item.id == id) + #expect(item.id == loadingItem.id) guard case .loading = item.state else { throw CartTestError.unexpectedItemStateInCart } @@ -140,11 +140,11 @@ struct PointOfSaleAggregateModelTests { @Test func updateLoadingItem_updates_loading_item_with_simple_product() async throws { // Given var cart = Cart() - let id = cart.addLoadingItem() + let loadingItem = cart.addLoadingItem() let purchasableItem = makePurchasableItem(name: "Test Product") // When - cart.updateLoadingItem(id: id, with: purchasableItem) + cart.updateLoadingItem(id: loadingItem.id, with: purchasableItem) // Then #expect(cart.purchasableItems.count == 1)