A React Native WebView debugging story about LCP, data URLs, and trace attributes
We recently hit a confusing Sentry performance issue in a React Native app:
The LCP transaction existed, but Trace Explorer could not find it by the attributes we attached to it.
The culprit was one span attribute:
lcpUrl = data:image/png;base64,...
In one iOS sample, that value was roughly 114KB before Sentry normalized it.
The symptom
We measure WebView page performance by reporting custom Sentry transactions for FCP and LCP:
<page> (FCP)
<page> (LCP)
Both use the same operation:
ui.web_page_load
And both include attributes we use for grouping and filtering:
metricInfo
pageTitle
pageUrl
host
path
durationMs
The strange part: Sentry Transaction Summary could show the LCP transaction, but Trace Explorer could not find the same data when filtering by attributes such as metricInfo, pageTitle, or path.
FCP worked. Android worked. iOS LCP did not.
The raw event told the story
After pulling the raw event from the Sentry API, the LCP transaction was clearly there. The transaction name was correct.
But the trace attributes were not complete. Some diagnostic fields were present:
durationMs
host
lcpElement
lcpUrl
But several fields required for dashboard queries were missing:
metricInfo
pageTitle
pageUrl
path
The suspicious field was lcpUrl. On iOS, it was not a normal URL. It was a base64 image data URI:
data:image/png;base64,...
Sentry marked the oversized value as limited. After that, the event still existed, but the attributes we depended on for aggregation were not queryable in the way we expected.
That explains the apparent contradiction:
- Transaction Summary could still find the transaction by name.
- Trace Explorer could not find it by the missing attributes.
Why Android looked fine
This part was easy to misread. The Android data looked healthy, so it was tempting to assume the instrumentation was fine.
It was not.
In our production samples, the same lcpUrl field looked very different by platform:
| Platform |
lcpUrl length |
Attribute query fields |
|---|---|---|
| iOS WebView | about 114KB | missing |
| Android WebView | 100 characters | present |
To isolate the difference, we built a small WebView test page with a large base64 image as the LCP candidate. The full image data URI was about 1.6MB.
On Android, the DOM and bridge could still carry the full string, but the LCP entry itself exposed only a 100-character URL:
DOM img.src.length = about 1.6MB
Android bridge received value = about 1.6MB
PerformanceObserver entry.url = 100 characters
So Android was not safe because our telemetry model was good. It was safe because Chromium WebView had already returned a short value for entry.url before we sent it to Sentry.
iOS Safari WebView returned the full data URI. That may be a reasonable browser behavior, but it was operationally dangerous for telemetry.
I would treat the Android behavior as an implementation detail, not a contract. Application code should not rely on a browser silently shortening a dangerous value.
The fix
We removed lcpUrl and lcpElement from the Sentry span attributes.
Before:
reportWebSpan("LCP", lcpValue, {
lcpElement,
lcpUrl,
});
After:
reportWebSpan("LCP", lcpValue);
We kept only small, stable attributes that are useful for grouping:
metricInfo
pageTitle
pageUrl
host
path
durationMs
After removing the oversized fields, LCP appeared correctly in the dashboard query again.
Lessons learned
Observability data needs stricter rules than ordinary application data.
A field is not safe just because the browser exposes it. Before sending it as a span attribute, ask:
- Is it bounded in size?
- Is it low-cardinality?
- Does it help answer a real production question?
- Could one bad value make the event harder to index or query?
For WebView performance telemetry, these are good attributes:
metricInfo = LCP
pageTitle = <stable page name>
host = example.com
path = /some/path
durationMs = 3020
These are dangerous attributes:
lcpUrl = data:image/png;base64,...
outerHTML = <large DOM subtree>
requestBody = ...
The dashboard was not wrong. The telemetry model was wrong.
And in this case, deleting two fields made the metric visible again.
Top comments (0)