<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: FrankFan</title>
    <description>The latest articles on Forem by FrankFan (@frankcode).</description>
    <link>https://forem.com/frankcode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1295265%2F08d30100-22df-4d06-9edb-375cadd92445.png</url>
      <title>Forem: FrankFan</title>
      <link>https://forem.com/frankcode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/frankcode"/>
    <language>en</language>
    <item>
      <title>Why Your WebView LCP Data Drops on iOS (But Survives on Android)</title>
      <dc:creator>FrankFan</dc:creator>
      <pubDate>Fri, 22 May 2026 10:34:25 +0000</pubDate>
      <link>https://forem.com/frankcode/the-114kb-span-attribute-that-hid-our-lcp-data-3a9h</link>
      <guid>https://forem.com/frankcode/the-114kb-span-attribute-that-hid-our-lcp-data-3a9h</guid>
      <description>&lt;h2&gt;
  
  
  A React Native WebView debugging story about LCP, data URLs, and trace attributes
&lt;/h2&gt;

&lt;p&gt;We recently hit a confusing Sentry performance issue in a React Native app:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The LCP transaction existed, but Trace Explorer could not find it by the attributes we attached to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The culprit was one span attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lcpUrl = data:image/png;base64,...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In one iOS sample, that value was roughly &lt;strong&gt;114KB&lt;/strong&gt; before Sentry normalized it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;We measure WebView page performance by reporting custom Sentry transactions for FCP and LCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;page&amp;gt; (FCP)
&amp;lt;page&amp;gt; (LCP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both use the same operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ui.web_page_load
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And both include attributes we use for grouping and filtering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metricInfo
pageTitle
pageUrl
host
path
durationMs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;metricInfo&lt;/code&gt;, &lt;code&gt;pageTitle&lt;/code&gt;, or &lt;code&gt;path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;FCP worked. Android worked. iOS LCP did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The raw event told the story
&lt;/h2&gt;

&lt;p&gt;After pulling the raw event from the Sentry API, the LCP transaction was clearly there. The transaction name was correct.&lt;/p&gt;

&lt;p&gt;But the trace attributes were not complete. Some diagnostic fields were present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;durationMs
host
lcpElement
lcpUrl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But several fields required for dashboard queries were missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metricInfo
pageTitle
pageUrl
path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suspicious field was &lt;code&gt;lcpUrl&lt;/code&gt;. On iOS, it was not a normal URL. It was a base64 image data URI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data:image/png;base64,...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That explains the apparent contradiction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction Summary could still find the transaction by name.&lt;/li&gt;
&lt;li&gt;Trace Explorer could not find it by the missing attributes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Android looked fine
&lt;/h2&gt;

&lt;p&gt;This part was easy to misread. The Android data looked healthy, so it was tempting to assume the instrumentation was fine.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;In our production samples, the same &lt;code&gt;lcpUrl&lt;/code&gt; field looked very different by platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;lcpUrl&lt;/code&gt; length&lt;/th&gt;
&lt;th&gt;Attribute query fields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iOS WebView&lt;/td&gt;
&lt;td&gt;about 114KB&lt;/td&gt;
&lt;td&gt;missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android WebView&lt;/td&gt;
&lt;td&gt;100 characters&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;On Android, the DOM and bridge could still carry the full string, but the LCP entry itself exposed only a 100-character URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOM img.src.length            = about 1.6MB
Android bridge received value = about 1.6MB
PerformanceObserver entry.url = 100 characters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So Android was not safe because our telemetry model was good. It was safe because Chromium WebView had already returned a short value for &lt;code&gt;entry.url&lt;/code&gt; before we sent it to Sentry.&lt;/p&gt;

&lt;p&gt;iOS Safari WebView returned the full data URI. That may be a reasonable browser behavior, but it was operationally dangerous for telemetry.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;We removed &lt;code&gt;lcpUrl&lt;/code&gt; and &lt;code&gt;lcpElement&lt;/code&gt; from the Sentry span attributes.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;reportWebSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LCP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lcpValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lcpElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lcpUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;reportWebSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LCP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lcpValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We kept only small, stable attributes that are useful for grouping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metricInfo
pageTitle
pageUrl
host
path
durationMs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After removing the oversized fields, LCP appeared correctly in the dashboard query again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;Observability data needs stricter rules than ordinary application data.&lt;/p&gt;

&lt;p&gt;A field is not safe just because the browser exposes it. Before sending it as a span attribute, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it bounded in size?&lt;/li&gt;
&lt;li&gt;Is it low-cardinality?&lt;/li&gt;
&lt;li&gt;Does it help answer a real production question?&lt;/li&gt;
&lt;li&gt;Could one bad value make the event harder to index or query?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For WebView performance telemetry, these are good attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metricInfo = LCP
pageTitle = &amp;lt;stable page name&amp;gt;
host = example.com
path = /some/path
durationMs = 3020
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are dangerous attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lcpUrl = data:image/png;base64,...
outerHTML = &amp;lt;large DOM subtree&amp;gt;
requestBody = ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard was not wrong. The telemetry model was wrong.&lt;/p&gt;

&lt;p&gt;And in this case, deleting two fields made the metric visible again.&lt;/p&gt;

</description>
      <category>observability</category>
      <category>sentry</category>
      <category>reactnative</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
