Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Woo POS] MVP order, cart, and miscellaneous events #15047

Merged
merged 26 commits into from
Feb 6, 2025

Conversation

iamgabrielma
Copy link
Contributor

@iamgabrielma iamgabrielma commented Feb 3, 2025

Closes: #13613
Closes: #15018

Description

This PR adds the POS track events discussed on pdfdoF-6hn-p2 for MVP. Final events and properties are still TBD, so most likely we'll need to update some before launch. Events are not validated yet, I'll do that when we have a final decision on all events and their properties, as its very time consuming.

You should see all events decorated with pos_* in the console, and as woocommerceios_pos_* in Track events.

I've opted to call analytics from ServiceLocator in most views. I'm sure we can inject some of them directly or through the viewModel but didn't seem worthy just for a single event or two. Happy to iterate on this as needed.

Testing

Go through POS performing different actions like adding, removing items from cart, checkout, create order, pay by cash or card, send receipt, exit POS, get support, etc, ... I've tested all of them manually, and the ones where we inject analytics have unit tests as well, so feel free to go through all of them or just some.

  • order_creation_success
🔵 Tracked pos_order_creation_success, properties: [plan: , blog_id: -1, was_ecommerce_trial: false, is_wpcom_store: false, site_url: https://indiemelon.mystagingwebsite.com, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]
  • order_creation_failed
🔵 Tracked pos_order_creation_failed, properties: [plan: , store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, was_ecommerce_trial: false, site_url: https://indiemelon.mystagingwebsite.com, is_wpcom_store: false, blog_id: -1, error_context: updateOrderFailed, use_gift_card: false, error_description: The operation couldn’t be completed. (Yosemite.POSOrderService.(unknown context at $10975b684).POSOrderServiceError error 1.)]
  • create_new_order_tapped
🔵 Tracked pos_create_new_order_tapped, properties: [blog_id: -1, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, was_ecommerce_trial: false, site_url: https://indiemelon.mystagingwebsite.com, plan: , is_wpcom_store: false]
  • item_removed_from_cart
🔵 Tracked pos_item_removed_from_cart, properties: [plan: , was_ecommerce_trial: false, site_url: https://indiemelon.mystagingwebsite.com, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, blog_id: -1, is_wpcom_store: false]
  • checkout_tapped (prop: items_in_cart with the number of items in cart when checkout is tapped)
🔵 Tracked pos_checkout_tapped, properties: [is_wpcom_store: false, blog_id: -1, items_in_cart: 1, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, was_ecommerce_trial: false, site_url: https://indiemelon.mystagingwebsite.com, plan: ]
  • back_to_cart_tapped
🔵 Tracked pos_back_to_cart_tapped, properties: [is_wpcom_store: false, blog_id: -1, plan: , was_ecommerce_trial: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, site_url: https://indiemelon.mystagingwebsite.com]
  • get_support_tapped
🔵 Tracked pos_get_support_tapped, properties: [plan: , blog_id: -1, was_ecommerce_trial: false, is_wpcom_store: false, site_url: https://indiemelon.mystagingwebsite.com, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]
  • exit_pos_menu_item_tapped
🔵 Tracked pos_exit_pos_menu_item_tapped, properties: [blog_id: -1, site_url: https://indiemelon.mystagingwebsite.com, plan: , was_ecommerce_trial: false, is_wpcom_store: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]
  • exit_pos_confirmed
🔵 Tracked pos_exit_pos_confirmed, properties: [site_url: https://indiemelon.mystagingwebsite.com, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, plan: , is_wpcom_store: false, was_ecommerce_trial: false, blog_id: -1]
  • simple_products_explanation_dialog_shown
🔵 Tracked pos_simple_products_explanation_dialog_shown, properties: [plan: , store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, blog_id: -1, is_wpcom_store: false, site_url: https://indiemelon.mystagingwebsite.com, was_ecommerce_trial: false]
  • email_receipt_tapped
🔵 Tracked pos_email_receipt_tapped, properties: [blog_id: -1, site_url: https://indiemelon.mystagingwebsite.com, is_wpcom_store: false, plan: , was_ecommerce_trial: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]
  • email_receipt_send_tapped
🔵 Tracked pos_email_receipt_send_tapped, properties: [plan: , blog_id: -1, site_url: https://indiemelon.mystagingwebsite.com, is_wpcom_store: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, was_ecommerce_trial: false]
  • products_pull_to_refresh
🔵 Tracked pos_products_pull_to_refresh, properties: [blog_id: -1, was_ecommerce_trial: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, is_wpcom_store: false, plan: , site_url: https://indiemelon.mystagingwebsite.com]
  • variations_pull_to_refresh
🔵 Tracked pos_variations_pull_to_refresh, properties: [site_url: https://indiemelon.mystagingwebsite.com, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, is_wpcom_store: false, was_ecommerce_trial: false, blog_id: -1, plan: ]
  • pos_loaded (prop milliseconds_time_elapsed_in_splash_screen with the milliseconds it took between view appear and disappear )
🔵 Tracked pos_pos_loaded, properties: [was_ecommerce_trial: false, is_wpcom_store: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, site_url: https://indiemelon.mystagingwebsite.com, plan: , milliseconds_time_elapsed_in_splash_screen: 1995.630126953125, blog_id: -1]

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

Reviewer (or Author, in the case of optional code reviews):

Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement:

  • The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
  • Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
  • Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on all devices (phone/tablet) and no regressions are added.

@dangermattic
Copy link
Collaborator

dangermattic commented Feb 3, 2025

2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is assigned to the milestone 21.7. This milestone is due in less than 2 days.
Please make sure to get it merged by then or assign it to a milestone with a later deadline.
1 Message
📖

This PR contains changes to Tracks-related logic. Please ensure (author and reviewer) the following are completed:

  • The tracks events must be validated in the Tracks system.
  • Verify the internal Tracks spreadsheet has also been updated.
  • Please consider registering any new events.
  • The PR must be assigned the category: tracks label.

Generated by 🚫 Danger

@iamgabrielma iamgabrielma added category: tracks Related to analytics, including Tracks Events. feature: POS labels Feb 3, 2025
@iamgabrielma iamgabrielma added this to the 21.7 milestone Feb 3, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Feb 3, 2025

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr15047-730be1b
Version21.6
Bundle IDcom.automattic.alpha.woocommerce
Commit730be1b
App Center BuildWooCommerce - Prototype Builds #12829
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

We already track this event by checking the tap on the support button via “pos_get_support_tapped”. But “support_new_request_viewed” will get decorated with “pos_” as we do not exit POS mode to send the support request.
Declaring WooAnalytics as dependency at file level makes the test suite crash when ran as a whole but not independently test by test. This is resolved by injecting the dependency at the test level, most likely a threading issue?
@iamgabrielma iamgabrielma marked this pull request as ready for review February 4, 2025 14:19
Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling all the POS analytics! I took some time to catch up on the previous analytics changes during AFK. I left some comments/questions, most of the testing worked well with two issues below.

  • pos_loaded (prop milliseconds_time_elapsed_in_splash_screen)
  • pos_simple_products_explanation_dialog_shown
  • get_support_tapped
  • pos_products_pull_to_refresh
  • pos_variations_pull_to_refresh
  • pos_exit_pos_menu_item_tapped
  • pos_exit_pos_confirmed
  • pos_checkout_tapped with items_in_cart prop
  • pos_order_creation_success
  • pos_email_receipt_tapped
  • pos_email_receipt_send_tapped
  • pos_create_new_order_tapped
  • pos_item_removed_from_cart
  • pos_back_to_cart_tapped

Issues from testing:

  • If I turn off the internet after adding items to cart and then tap “Check out,” pos_order_creation_failed was tracked twice
  • In the simple products dialog: when I tapped “Create an order in store management” in the dialog and the orders tab was shown, pos_orders_add_new got logged unexpectedly (shouldn’t have the pos prefix).
    • After that, I tried going back to the Menu tab but tapping on POS entry point was no-op (hub_menu_option_tapped with option: pointOfSale was logged but POS wasn’t shown). However, I couldn’t reproduce this.

Other tracking events I saw:

  • pos_item_added_to_cart with product_type prop
  • pos_card_reader_connection_success
  • pos_card_present_collect_payment_canceled
  • pos_card_present_collect_payment_success

Questions:

  • I didn’t see an event tracked when tapping “Connect your reader.” Is this something we want to track, maybe also with the connection state?
    • Similarly, the “Connect to reader” CTA in the totals screen.
  • Do we want to track the CTA tap to clear the cart?
  • Do we want to track the cash payment flow? Like tapping to pay with cash and completion etc.
  • Do we want to track when the success screen is shown?

@iamgabrielma
Copy link
Contributor Author

Thanks for the review @jaclync , this is ready for another round when you have the time. I believe I answered all items below, let me know if I missed something:

If I turn off the internet after adding items to cart and then tap “Check out,” pos_order_creation_failed was tracked twice

I cannot replicate this one. By using the Network Link Conditioner with a 100% loss profile as well as disconnecting WIFI manually I seem to track this once only:

🔵 Tracked pos_order_creation_failed, properties: [error_description: URLSessionTask failed with error: The request timed out., was_ecommerce_trial: false, blog_id: -1, is_wpcom_store: false, use_gift_card: false, plan: , site_url: https://indiemelon.mystagingwebsite.com, error_context: sessionTaskFailed(error: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2102, NSUnderlyingError=0x600000d3c690 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <3691169B-7429-4B45-A3D9-42F15C65CDD9>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
                     "LocalDataTask <3691169B-7429-4B45-A3D9-42F15C65CDD9>.<1>"
                 ), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https://indiemelon.mystagingwebsite.com/?rest_route=/wc/v3/orders, NSErrorFailingURLKey=https://indiemelon.mystagingwebsite.com/?rest_route=/wc/v3/orders, _kCFStreamErrorDomainKey=4}), store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]

If I switch the connection back on before the nw_socket_handle_socket_event [C34.1.2:3] Socket SO_ERROR [60: Operation timed out] is called, then it processes the order correctly and logs success, otherwise failed is only logged once. Does it happen often on your end?

In the simple products dialog: when I tapped “Create an order in store management” in the dialog and the orders tab was shown, pos_orders_add_new got logged unexpectedly (shouldn’t have the pos prefix).

Good catch, added to the list here: 3f6ae93

After that, I tried going back to the Menu tab but tapping on POS entry point was no-op (hub_menu_option_tapped with option: pointOfSale was logged but POS wasn’t shown). However, I couldn’t reproduce this.

I couldn't neither :/

I didn’t see an event tracked when tapping “Connect your reader.” Is this something we want to track, maybe also with the connection state? Similarly, the “Connect to reader” CTA in the totals screen.

Ahhh this one went missing! As is not tracked through the card reader service . I've added both events and tests here: a4152d0

Do we want to track the CTA tap to clear the cart?

We do, I missed this one or I was waiting for the rest of cart events. I've added it here: f4509cc

Do we want to track the cash payment flow? Like tapping to pay with cash and completion etc.

Yes, we do, but the events are TBD for now. These are logged here: #15057

Do we want to track when the success screen is shown?

I'm not sure, the cash or card succeed might be enough. I'll ask in the P2.

Could onAppear/onDisappear be triggered when putting the app to the background then back, or switching to another app then back etc.? What if the loading view is shown outside of the initial load? From PointOfSaleItemsController, it sets the container state to .loading which shows this loading view when loading products when there are no existing products. So if there are no products in the beginning from the initial load, then triggering a reload, this view is shown again.

That's a good question: By backgrounding the app or switching and then back I cannot see the PointOfSaleLoadingView re-rendering, neither when we refresh products after loading an empty list via pull-to-refresh. I'd expect so from the code in the dashboard state, as we fall into the .loading case but it doesn't seem to render the view? (Semi-related, if I return .init(items: [], hasMorePages: false) from PointOfSaleItemsController.loadItems I cannot pull-to-refresh, is this expected?). So we only seem to track it once on entering the POS.

Moving the event to, for example, PointOfSaleItemsController.loadItems seems to make it trickier since now we do call it each time we load from store. Declaring it as nil and then assigning it would help us to only call it once if we can assure that the controller is only loaded once, however, we'll have to call .end() each time the load products, logging events we do not want. Knowing this, do you have any recommendation about how we could track the initial load only once if it's not using appear/disappear?

@iamgabrielma iamgabrielma requested a review from jaclync February 5, 2025 13:06
}

private enum Keys {
static let waitingTime = "waiting_time"
static let milliseconds_time_elapsed = "milliseconds_time_elapsed_in_splash_screen"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as the value is pretty specific to "splash screen", probably want to name the key accordingly. Also would be great to keep it camel case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True that, updated: 47924ea

@@ -21,6 +41,9 @@ private extension PointOfSaleLoadingView {
static let textSpacing: CGFloat = 16
static let progressViewSpacing: CGFloat = 72
}
enum Constants {
static let toMilliseconds: Double = 1000.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this doesn't seem needed anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! 9c57d76

@jaclync
Copy link
Contributor

jaclync commented Feb 6, 2025

In the simple products dialog: when I tapped “Create an order in store management” in the dialog and the orders tab was shown, pos_orders_add_new got logged unexpectedly (shouldn’t have the pos prefix).

Good catch, added to the list here: 3f6ae93

After that, I tried going back to the Menu tab but tapping on POS entry point was no-op (hub_menu_option_tapped with option: pointOfSale was logged but POS wasn’t shown). However, I couldn’t reproduce this.

I couldn't neither :/

Actually, I just encountered this in the first run of PR testing again today. I did some debugging, but couldn't figure out the cause in the time I had: the fullScreenCover content was called to display the POS entry point view, but the actual view was hidden and the navigation stack in the view hierarchy was grayed out:

Screenshot 2025-02-06 at 2 46 22 PM

I also recalled the order creation form not being shown in the orders tab. It might be some UI issue related to race conditions.

@jaclync
Copy link
Contributor

jaclync commented Feb 6, 2025

If I turn off the internet after adding items to cart and then tap “Check out,” pos_order_creation_failed was tracked twice

I cannot replicate this one. By using the Network Link Conditioner with a 100% loss profile as well as disconnecting WIFI manually I seem to track this once only:

🔵 Tracked pos_order_creation_failed, properties: [error_description: URLSessionTask failed with error: The request timed out., was_ecommerce_trial: false, blog_id: -1, is_wpcom_store: false, use_gift_card: false, plan: , site_url: https://indiemelon.mystagingwebsite.com, error_context: sessionTaskFailed(error: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2102, NSUnderlyingError=0x600000d3c690 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <3691169B-7429-4B45-A3D9-42F15C65CDD9>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
                     "LocalDataTask <3691169B-7429-4B45-A3D9-42F15C65CDD9>.<1>"
                 ), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https://indiemelon.mystagingwebsite.com/?rest_route=/wc/v3/orders, NSErrorFailingURLKey=https://indiemelon.mystagingwebsite.com/?rest_route=/wc/v3/orders, _kCFStreamErrorDomainKey=4}), store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]

If I switch the connection back on before the nw_socket_handle_socket_event [C34.1.2:3] Socket SO_ERROR [60: Operation timed out] is called, then it processes the order correctly and logs success, otherwise failed is only logged once. Does it happen often on your end?

Ah never mind, the multiple pos_order_creation_failed events were from retrying while the device was still offline.

@jaclync
Copy link
Contributor

jaclync commented Feb 6, 2025

Could onAppear/onDisappear be triggered when putting the app to the background then back, or switching to another app then back etc.? What if the loading view is shown outside of the initial load? From PointOfSaleItemsController, it sets the container state to .loading which shows this loading view when loading products when there are no existing products. So if there are no products in the beginning from the initial load, then triggering a reload, this view is shown again.

That's a good question: By backgrounding the app or switching and then back I cannot see the PointOfSaleLoadingView re-rendering, neither when we refresh products after loading an empty list via pull-to-refresh. I'd expect so from the code in the dashboard state, as we fall into the .loading case but it doesn't seem to render the view? (Semi-related, if I return .init(items: [], hasMorePages: false) from PointOfSaleItemsController.loadItems I cannot pull-to-refresh, is this expected?). So we only seem to track it once on entering the POS.

Maybe it will never reach the state to show this loading view, it looks like when there are no eligible products this view is shown:

Is this the view you mentioned there is no PTR?

In any case, if the UI logic changes in the future to show this loading view, the event might be unintentionally tracked.

Moving the event to, for example, PointOfSaleItemsController.loadItems seems to make it trickier since now we do call it each time we load from store. Declaring it as nil and then assigning it would help us to only call it once if we can assure that the controller is only loaded once, however, we'll have to call .end() each time the load products, logging events we do not want. Knowing this, do you have any recommendation about how we could track the initial load only once if it's not using appear/disappear?

I was thinking about adding a private boolean variable to track whether the initial load event has been tracked + unit tests to ensure only the initial load is tracked. Any other cleaner solutions would be great too.

Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! LGTM once the ❕ comment is addressed.

@@ -2504,6 +2506,8 @@ extension WooAnalyticsEvent {
return WooAnalyticsEvent(statName: .analyticsHubWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
case .appStartup:
return WooAnalyticsEvent(statName: .applicationOpenedWaitingTimeLoaded, properties: [Keys.waitingTime: elapsedTime])
case .pointOfSaleLoaded:
return WooAnalyticsEvent(statName: .pointOfSaleLoaded, properties: [Keys.milliseconds_time_elapsed: elapsedTime])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❕ During testing it looks like the prop is recording the time in seconds, whereas it's expected to be milliseconds:

🔵 Tracked pos_pos_loaded, properties: [milliseconds_time_elapsed_in_splash_screen: 2.351064920425415, ...]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's not the case, is it? I might be reading this wrongly but when we log the event on WaitingTimeTracker we log the elapsed time, so in this case we're still capturing it in milliseconds right? So 1.9ms in this instance, which we pass to the event:

po currentTimeInMillis()
> 1738832706.298878

po waitingStartedTimestamp
> 1738832690.2819052

po elapsedTime (as currentTimeInMillis() - waitingStartedTimestamp)
> 1.9627389907836914

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the calculation seems off too 1738832706.298878 - 1738832690.2819052 it's not what we get but ~16, checking as I have just used the WaitingTimeTracker that was already there 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 4d49861 I've updated the interface of WaitingTimeTracker as there was a very miss-leading property in the initializer: currentTimeInMillis captures the time in seconds, not in milliseconds.

I've added a new endInMilliseconds() method just for our use case, so I avoid touching the current implementation much, and now captures the elapsed time properly:

🔵 Tracked pos_pos_loaded, properties: [is_wpcom_store: false, was_ecommerce_trial: false, milliseconds_time_elapsed_in_splash_screen: 2172.056198120117, site_url: https://indiemelon.mystagingwebsite.com, plan: , store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f, blog_id: -1]

@iamgabrielma
Copy link
Contributor Author

Thanks again for the review! I just have a question regarding the milliseconds tracking here.

Is this the view you mentioned there is no PTR?
In any case, if the UI logic changes in the future to show this loading view, the event might be unintentionally tracked.
I was thinking about adding a private boolean variable to track whether the initial load event has been tracked + unit tests to ensure only the initial load is tracked. Any other cleaner solutions would be great too.

Yes, that's the one. It also seems we're removing the .loading state from PTR as we move to Observable, so I'll hold this change a bit so it doesn't get too messy. I'll handle that event separately to be sure it's only called on POS start.

@iamgabrielma iamgabrielma merged commit 422fb6a into trunk Feb 6, 2025
12 checks passed
@iamgabrielma iamgabrielma deleted the task/13277-pos-track-miscelaneous-events branch February 6, 2025 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
category: tracks Related to analytics, including Tracks Events. feature: POS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Woo POS] MVP Analytics: Miscellaneous events [Woo POS] MVP Analytics: Order events
4 participants