-
Notifications
You must be signed in to change notification settings - Fork 996
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
Embedded GA metrics #4507
Embedded GA metrics #4507
Conversation
StripePaymentSheet/StripePaymentSheet/Source/Analytics/PaymentSheetAnalyticsHelper.swift
Outdated
Show resolved
Hide resolved
@@ -115,6 +115,12 @@ public final class EmbeddedPaymentElement { | |||
_ = await latestUpdateTask?.value | |||
// Start the new update task | |||
let currentUpdateTask = Task { @MainActor [weak self, configuration, analyticsHelper] in | |||
analyticsHelper.logEmbeddedUpdateStarted() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why log here and not at the start of the update
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! I think we should log when the update work actually begins not when it is scheduled. If we log it when it is scheduled (at the top of the function) it makes it harder to reason about the duration metric, since some will account for awaiting the previous update canceling and some won't. If we want consistency of the duration metric across all update events we should only measure the time the actual work associated with the update took, not accounting for any time it spent waiting for a previous update to cancel.
It's also convenient from an implementation standpoint with how we currently handle dates in the analytics helper. If we allow multiple update start events to be fired before the finished event it will overwrite the updateStartDate before the previous finished event could send.
If we fire the start event at the top we could get this
mc_embedded_update_started -> set's updateStartDate to x
mc_embedded_update_started -> set's updateStartDate to y
mc_embedded_update_finished -> reads updateStartDate set to y
mc_embedded_update_finished -> reads updateStartDate set to y
We could get around this by mapping update events and their start dates to a UUID so we know what start date to compute the duration on for all the finished events. But, putting the analytics in the Task ensures the ordering is correct/consistent which I prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we moved this to the start of update
, duration represents the time it takes for the update
method to finish. That feels quite simple and straightforward.
The total time the app spends waiting for update
to finish also seems like a more useful duration metric. Like, why ignore the time spent waiting for previous update calls to finish? That's still time the app spends waiting and something we control (e.g. we could try to cancel in-flight tasks more aggressively).
Moving to the top has the additional benefit of capturing all possible errors.
Re: implementation - what if we keep track of the start time within the update
method?
analyticsHelper.logEmbeddedUpdateStarted() | ||
var updateResult: UpdateResult = .canceled // Default to canceled if unhandled | ||
defer { | ||
analyticsHelper.logEmbeddedUpdateFinished(result: updateResult) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm can we not log the result immediately before this method returns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong, but I believe there is a race condition. By the time you reach logEmbeddedUpdateFinished(result:)
at the end of the function, a second update
could have already started its own update and logged a start event. There’s no guarantee about which event comes first unless you serialize the calls within the same Task. Logging right before the method returns can record an “update finished” event out of order, or with an incorrect start date, if another update began in between.
latestUpdateTask?.cancel()
_ = await latestUpdateTask?.value
// Define task
let updateResult = await currentUpdateTask.value
analyticsHelper.logEmbeddedUpdateFinished(result: updateResult)
// After we await the update task we cannot guarantee this code runs before the next update task starts and logs an update started event, resetting the updateStartDate.
As I write this out, maybe we should just map update calls to a UUID and a start date to avoid this, but logging all the update events within the Task seems the safest with the current way we handle dates in the analytics helper. Even with that approach, we could get two update start events sent before an update finishes event, putting all the analytics in the Task ensures the order is correct/deterministic and makes it easier to reason about the ordering of the events, so I slightly prefer putting them within the Task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine the merchant does the following
- Call
update
- Call
update
again before (1) finishes. - Receive Update (1) result
- Receive Update (2) result
I think you're saying the correct analytics ordering is:
- update_started
- update_finished
- update_started
- update_finished
That matches the ordering of the Tasks inside the update
method.
I'm proposing (along with my other comment above) to model this analytic around the entire update method, not just the inner Task. This would make the ordering match the merchant's POV:
- update_started
- update_started
- update_finished
- update_finished
The ordering is still deterministic; update_started
fires when the update
API is called and update_finished
fires when it returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure we can do that 👍
Summary
Motivation
Testing
Changelog
N/A