Instrument Swift checkout failures - #675
Conversation
8c0ff67 to
e36641b
Compare
0bdb099 to
da2ae5b
Compare
e36641b to
ffd7c4d
Compare
da2ae5b to
0611c9e
Compare
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Install this buildOpen Tophat, select your target device, then click Install. Links open on the Mac running Tophat.
Checkout Kit E2E results
|
| let client = CheckoutKitTelemetry( | ||
| configuration: .init( | ||
| sdkVersion: MetaData.version, | ||
| product: entryPoint == .acceleratedCheckouts ? .acceleratedCheckouts : .checkoutKit, |
There was a problem hiding this comment.
Does product just become a piece of metadata in the metrics?
Just wondering if theres a benefit to having N clients per entrypoint or if it makes sense to just share 1
thinking to any future products we add, does it make sense to keep many permutations inside the telemetry library, or could each extra library just attach a dict to identify themselves/attach info they want
There was a problem hiding this comment.
Went with your suggestion — one shared client now. product was already a per-datapoint attribute on the wire, so the client takes it per measurement and recorder(for:) just stamps the entry point's product. On the dict idea, I'd rather not: arbitrary attributes would break the closed-cardinality contract, so a new product stays an enum case.
ffd7c4d to
0084629
Compare
0611c9e to
4478383
Compare
0084629 to
9e37951
Compare
4478383 to
3eb6a64
Compare
There was a problem hiding this comment.
This is looking good to me. I just have a couple of questions around:
- Ownership - feels unusual to me that the telemetry client belongs to the CheckoutWebView, rather than be a global.
- Having an instance of the telemetry client per entrypoint
- Offering the ability to swap out the configured telemetry client. I don't think we want this. The most we want to offer at the moment is the ability to turn it on or off.
| } | ||
|
|
||
| func keepAliveDidFail() { | ||
| entry?.view.telemetryRecorder.recordError( |
There was a problem hiding this comment.
Feels strange to me that the telemetry class is bound to the checkout view. What's the reason for that? Can't it be a global instance?
Also a small nit: I find the name telemetryRecorder a bit awkward. Can we make it telemetry instead?
| entry?.view.telemetryRecorder.recordError( | |
| entry?.view.telemetry.recordError( |
There was a problem hiding this comment.
It is global — the view property is just a computed accessor onto the one shared client, scoped to the view's entry point (the view is the only thing that knows it). Renamed to telemetry as suggested.
| CheckoutTelemetry.recorder(for: entryPoint).recordProtocolDecodeError( | ||
| .init(method: .init(method: method), failureType: .params) | ||
| ) |
There was a problem hiding this comment.
I'm not totally following why we have a telemetry instance for each "entrypoint" - @tiagocandido can you help me understand this?
There was a problem hiding this comment.
Not an instance per entry point — recorder(for:) hands out a stateless struct that stamps the right product on each measurement before forwarding to the single shared client. The wrapper exists so accelerated checkouts and regular checkout split on the product dimension without a second client.
| } | ||
|
|
||
| didRetryCheckoutNavigation = true | ||
| let retryReason = CheckoutTelemetry.retryReason(for: nsError) |
There was a problem hiding this comment.
Is there a way we could bake this into the recordNavigationRetry method? It feels like we're exposing an internal concern here, by having the telemetry class compute the reason and then feed it back into the class via another method
There was a problem hiding this comment.
Good call — added TelemetryNavigationRetryMetric(error:result:) so the mapping lives with the metric, and the call site stores retry.reason from the metric it just recorded. The view still has to remember the reason, since the later .failed event and the is_retry flags must report the same one across delegate callbacks.
| /// Controls anonymous diagnostic metrics sent by Checkout Kit. | ||
| public var telemetry = Configuration.Telemetry() |
There was a problem hiding this comment.
How come this is public and configurable? I would expect this to be internal
There was a problem hiding this comment.
It's public deliberately — this is the consumer opt-out (configuration.telemetry.enabled = false), documented in the README, and it's what the React Native telemetry: boolean config maps onto. Making it internal would remove the opt-out.
| extension Configuration { | ||
| public struct Telemetry: Sendable { | ||
| /// Set to `false` to prevent Checkout Kit from recording or sending diagnostic metrics. | ||
| public var enabled: Bool = true | ||
| } | ||
| } |
There was a problem hiding this comment.
This seems right, but giving the ability to swap out the telemetry client feels wrong
There was a problem hiding this comment.
Nothing here is swappable — enabled is the struct's only public member and it has no public initializer, so flipping the Bool is the only thing a consumer can do. No client or recorder type is public; the single injection seam is an internal test-only override. Same shape as preloading.enabled a few lines up.
| platform: telemetryPlatform() | ||
| ) | ||
| ) | ||
| client.start() |
There was a problem hiding this comment.
Seems like we could have it start implicitly in the telemetry constructor
There was a problem hiding this comment.
Deliberate: constructing and exporting are separate states in all three languages. Web relies on it directly — the element creates the client lazily but only arms the 60s export timer on connectedCallback, with a test pinning that disabled-before-connection never starts — and the foundations tests build clients without live timers. Starting in the constructor would collapse that state, so I'd keep the one explicit start() here.
3eb6a64 to
5a27e7c
Compare
|
Thanks Mark — on the three themes:
|

Summary
started,failed,not_attempted)productattribute (accelerated_checkoutsvscheckout_kit) onto each measurement, and the platform reportsreact-native-swiftwhen embedded in React Nativeis_retrycorrectly when the web content process terminates during a retried navigationparamsdecode failure for terminalec.errorpayloads that have a valid envelope but undecodable paramsValidation