Quick Win: Android: Improve PageLoadWideEvent reporting to recover 21.6% unknown events - #9574
Conversation
Flows were reaching the five-minute cleanup instead of a terminal state: - Key flows by navigationId, not url: a redirect changed the url under the flow, so its finish no longer matched and was dropped. - End the open flow when an untracked load starts, instead of leaving it to the cleanup policy. - Report a failure for every main-frame error. Gating on whether the error had a user-facing screen left most failures unreported. - End the measured load independently of the page load cycle, so a callback for an already-replaced load cannot orphan its replacement. - Reset the fixed-progress gate per load, closing its interval on same-host redirects. Failures match the load's url strictly, because marking a load failed also suppresses its finish. Renames onPageLoadCycleStarted to onMainFrameLoadStarted, which fires per load, and samples the request count per load for the event; the page load pixel keeps the per-cycle value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b4e4cb6. Configure here.
|
|
||
| private fun reportMeasuredLoadFinished() { | ||
| navigationId?.let { reportMeasuredLoadEnded(it, errorDescription = null) } | ||
| } |
There was a problem hiding this comment.
Finish closes replaced load
Medium Severity
reportMeasuredLoadFinished always ends the current navigationId, while failures only end a load when the error URL matches navigationUrl. After a replacement onPageStarted, a late onPageFinished for the previous URL can still close the new measured load as success whenever webView.progress == 100.
That undoes the “no later callback can claim this load” invariant for the success path, and can finish the wrong hop early (or leave the real completion with nothing to report). The error path already declines mismatched URLs for this reason; the finish path does not.
Please tell me if this was useful or not with a 👍 or 👎.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit b4e4cb6. Configure here.
GerardPaligot
left a comment
There was a problem hiding this comment.
Few optional comments but it works pretty well, I've tested additional scenarios:
- Load a website in error state. Expectation: Cancel the page load.
- Load the same website in the same tab before the end of the load. Expectation: Cancelling the previous one and send only the last one
- Load the same website in different tab. Expectation: Capture all events.
Everything is working fine!
| wideEventClient.flowAbort(existingState.flowId) | ||
| activeFlows.remove(tabId)?.let { previous -> | ||
| logcat { "Cancelling previous flow for tabId=$tabId, flowId=${previous.flowId} (${previous.url} → $url)" } | ||
| wideEventClient.flowAbort(previous.flowId) |
There was a problem hiding this comment.
This change can have a significant impact if we come from an observed page to another one. flowAbort sends nothing to our telemetry; what do you think about using flowFinish(status = FlowStatus.Cancelled) instead to capture the event, while making it clear that the page load was superseded by another one?
|
|
||
| // Needed for PageLoadWideEvent: it identifies the url the measured load started with, so a main frame error | ||
| // can be told apart from one reported against some other url. | ||
| private var navigationUrl: String? = null |
There was a problem hiding this comment.
Nit: BrowserWebViewClient is becoming increasingly complex (with these fields, but all related functions too). I'm pretty sure we can extract the page load logic elsewhere to handle the navigation ID/URL. What do you think about creating a dedicated task for this outside the Quick Win scope?
| if (request?.isForMainFrame == true) { | ||
| // Reported for every main-frame error, not just ones shown to the user. Otherwise, other failures | ||
| // never close properly and later get marked Unknown, making failed loads look like abandoned ones. | ||
| reportMeasuredLoadFailed( |
There was a problem hiding this comment.
For an OMITTED main-frame error, reportMeasuredLoadFailed ends the measured load while start stays non-null, so onPageFinished still fires pageLoadedHandler.onPageLoaded and the pixel counts a completed load, while the wide event recorded error.
I don't know if the existing pixel is still used by someone but we should consider to drop it in a dedicated task.



Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1217240546184827?focus=true
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable):
Description
The page load wide event was ending a large share of flows at the five-minute cleanup as
Unknowninstead of at a real outcome. Five separate causes:navigationId, not url. A redirect changed the url under the flow, so its finish no longer matched and was dropped.UrlUpdated, so the previous url-driven reset never ran).Failures are matched strictly on the load's url, because marking a load failed also suppresses its finish — an error reported against a different url is declined rather than guessed at.
No pixel definition change:
error_codeis already a free-form string documented as any WebViewClient error constant.Known limitation, deliberate. Every main-frame page start opens its own measured load, including a redirect hop, so a redirecting load is measured from its last hop:
elapsed_time_to_finishexcludes the redirect leg the user also waited through, and aborts count hops rather than loads the user abandoned. Confirmed on device —reddit.com/→reddit.com/?rdt=…drops 505 ms. Spanning a chain with one flow is possible viaWebResourceRequest.isRedirect, but it would move the content-scope and JS-injection steps onto the document the redirect left behind and change which urls are sampled. Both are decisions about what the metric should mean, so they are out of scope here.Steps to test this PR
Setup - IMPORTANT:
isFeatureEnabled()function inPageLoadWideEvent.ktwithprivate suspend fun isFeatureEnabled(): Boolean = trueso you can see events.PageLoadedSites.perfSitesare tracked (bbc.co.uk,reddit.com,foxnews.com,amazon.com,apnews.com, …). Any other domain logs nothing.adb logcat -v time | grep -E 'Page load measured as navigationId|Page load flow started|Cancelling previous flow|Page visible recorded|Exited max progress threshold|Recorded elapsed_time|Page load finished|Ignoring repeat|Ignoring load failure|Dropping event from navigation|No active flow found|Failed to start page loadflow'Successful load reaches a terminal outcome
Page load measured as navigationId [...]Page load finished [...]Flow id should match for the above logs. If https://www.bbc.com/news redirects to https://www.bbc.co.uk/news you should see 2 events, one for each page loaded.wide_page-load_cMultiple page loads, where one is cancelled should not record the cancelled one
NO UI changes
Note
Medium Risk
Telemetry-only change to page-load wide-event attribution and lifecycle; no user-facing or security impact, but it can shift measured durations and error vs abandoned rates.
Overview
Stops page-load wide events from timing out as Unknown by identifying each measured load with
navigationIdinstead of URL, so redirects and replacement navigations no longer drop or steal events.BrowserWebViewClientnow starts a new measured load on every main-frameonPageStarted(including redirect hops), reports finish/failure against that id, and keeps measurement independent of the page-load cycle. Main-frame errors always close the matching load; errors for a different URL are ignored. Starting any load (even untracked) aborts the tab’s previous flow.Progress “escaped fixed threshold” is reset per load via
onMainFrameLoadStarted, so same-host redirects get their own interval. Repeat steps on the same flow are ignored. Redirect chains are still measured from the last hop by design.Reviewed by Cursor Bugbot for commit b4e4cb6. Bugbot is set up for automated code reviews on this repo. Configure here.