Flare by Spatie
    • Error Tracking
    • Performance Monitoring
    • Logs
  • Pricing
  • Docs
  • Insights
  • Changelog
  • Back to Flare ⌘↵ Shortcut: Command or Control Enter
  • Sign in
  • Try Flare for free
  • Error Tracking
  • Performance Monitoring
  • Logs
  • Pricing
  • Docs
  • Insights
  • Changelog
    • Back to Flare ⌘↵ Shortcut: Command or Control Enter
    • Try Flare for free
    • Sign in
Flare Flare PHP PHP JavaScript JavaScript Protocol Protocol
JavaScript
  • PHP
  • JavaScript
  • General
  • Introduction
  • Sending data
  • Events
  • Errors
  • Attributes
  • Sourcemaps
  • Traces
  • Lifecycle
  • Web vitals
  • Aggregations
  • Shared by all clients
  • Entry points
  • Resources
  • Attribute formats
  • Errors payload
  • Traces payload
  • Logs payload

Web vitals

View as Markdown

Core Web Vitals reach Flare as attributes on a page's spans. They are spread over two spans because the browser does not know all of them at the same time.

The attributes

Every value is a number of milliseconds, except browser.web_vital.cls, which has no unit.

Attribute Type Description
browser.web_vital.lcp double Largest Contentful Paint, in milliseconds
browser.web_vital.cls double Cumulative Layout Shift
browser.web_vital.inp double Interaction to Next Paint, in milliseconds
browser.web_vital.fcp double First Contentful Paint, in milliseconds
browser.web_vital.ttfb double Time to First Byte, in milliseconds

If the browser never reported a vital, leave the attribute out. Do not send 0.

A Cumulative Layout Shift of 0 has meaning: it says the page did not shift at all, which is the best result a page can get. Only Chromium browsers measure it, so a client that sends 0 when it has nothing to report is telling Flare that every Firefox and Safari visit had a perfect layout. For that reason, leave out the value if it can not be measured instead of sending 0.

Why two spans

Some vitals are final the first time they report. Time to First Byte and First Contentful Paint happen once and never change.

The other three keep moving. Largest Contentful Paint can be replaced by a bigger element later. Cumulative Layout Shift adds up as the page shifts. Interaction to Next Paint needs the user to interact first, which may not have happened yet when the page finishes loading.

So a page reports its vitals twice:

  • Whatever is already final when the browser_pageload span closes gets added to that span.
  • Whatever is still moving gets sent later, on a browser_web_vital span, in a separate request.

Flare looks for the vital attributes on any span in the aggregation. It does not care which of the two carried them, so you can put any vital on either span. Just send each vital once per pageload. Put the same value on both spans and it gets counted twice.

Send the second request on the first client-side navigation or when the page is hidden, whichever happens first. If you only wait for page hide, you lose the vitals of anyone who navigated away inside your single page app. A request started while the page is closing needs keepalive to survive, see Sending data.

The browser_web_vital span

It is a container span. That is what lets it point at a parent Flare received in an earlier request.

Field Value
parentSpanId The spanId of the pageload span these vitals belong to
startTimeUnixNano The startTimeUnixNano of that pageload span
endTimeUnixNano The same value again, so the span has zero duration

Both timestamps sit in the past

You send the browser_web_vital span long after the page loaded, but you do not timestamp it at that moment. Both timestamps of the vitals span have to match the start of the pageload span.

Flare files vitals into a time bucket based on the span's start time. If you timestamped the span at page hide, a tab someone left open for forty minutes would report its vitals forty minutes late, into a bucket with no matching pageload. Vitals that land in a bucket on their own could be dropped.

Repeat the page context

Keep a reference to the pageload root for as long as the document lives. Building the vitals span needs its traceId, its spanId, its start time and the attributes it was opened with. Reading traceId and spanId off a span that has already ended is fine, so the root closing costs you nothing.

Copy the page's attributes onto the vitals span from that retained copy. Flare groups the span on its own attributes and never looks at the parent, so anything you leave out is missing:

Attribute Required Description
http.route yes The page's route, matching the pageload span
flare.entry_point.handler.identifier no The same route value again, matching the pageload span
flare.route.source no route or url, again matching the pageload span
flare.entry_point.type, flare.entry_point.value, flare.entry_point.handler.type no The rest of the entry point, copied from the pageload span
url.full, user_agent.original, http.request.referrer, document.ready_state no The page context, copied from the pageload span

Only http.route decides whether the span can be grouped. Without it Flare accepts the request and the vitals go nowhere. The rest is worth sending anyway, so the vitals span describes the same page its numbers came from.

Capture those attributes when the page opens, not when you send the vitals. By page hide the browser may have moved on, and the vitals belong to the page that was loaded.

Keep the retained copy up to date

A router that resolves its route pattern late is the awkward case. The pageload span may already have closed by then, and renaming a closed span does nothing, but the vitals span has not been built yet.

So when a route settles, update your retained copy even when the pageload root itself is past changing. Skip that and the pageload span reports /products/{id} while the vitals span reports /products/9f2c, which puts the vitals in a group of their own, separate from the page they measured.

Effect on page metrics

A browser_web_vital span does not count towards the pageload count, the average, the duration percentiles or the error rate of its aggregation. It only contributes its vital values. Sending one will not inflate your pageload count, and its zero duration will not drag your page timings down.

Example

The second request for the page load in the events example. Time to First Byte and First Contentful Paint were already added to the page span, so only the remaining three are here:

{
    "resourceSpans": [
        {
            "resource": {
                "attributes": [
                    { "key": "service.name", "value": { "stringValue": "Acme Storefront" } },
                    { "key": "telemetry.sdk.language", "value": { "stringValue": "javascript" } },
                    { "key": "telemetry.sdk.name", "value": { "stringValue": "@flareapp/js" } },
                    { "key": "flare.framework.name", "value": { "stringValue": "react" } }
                ],
                "droppedAttributesCount": 0
            },
            "scopeSpans": [
                {
                    "scope": { "name": "@flareapp/js", "version": "2.6.0" },
                    "spans": [
                        {
                            "traceId": "8d4f1a2b3c5e607988d4f1a2b3c5e079",
                            "spanId": "5e6f708192a3b4c5",
                            "parentSpanId": "1a2b3c4d5e6f7081",
                            "name": "/products/{id}",
                            "startTimeUnixNano": 1710252000000000000,
                            "endTimeUnixNano": 1710252000000000000,
                            "status": { "code": 0 },
                            "attributes": [
                                { "key": "flare.span_type", "value": { "stringValue": "browser_web_vital" } },
                                { "key": "flare.entry_point.type", "value": { "stringValue": "web" } },
                                { "key": "flare.entry_point.value", "value": { "stringValue": "https://acme.test/products/9f2c" } },
                                { "key": "flare.entry_point.handler.type", "value": { "stringValue": "browser" } },
                                { "key": "flare.entry_point.handler.identifier", "value": { "stringValue": "/products/{id}" } },
                                { "key": "http.route", "value": { "stringValue": "/products/{id}" } },
                                { "key": "flare.route.source", "value": { "stringValue": "route" } },
                                { "key": "url.full", "value": { "stringValue": "https://acme.test/products/9f2c" } },
                                { "key": "document.ready_state", "value": { "stringValue": "loading" } },
                                { "key": "browser.web_vital.lcp", "value": { "doubleValue": 1104 } },
                                { "key": "browser.web_vital.cls", "value": { "doubleValue": 0.04 } },
                                { "key": "browser.web_vital.inp", "value": { "doubleValue": 72 } }
                            ],
                            "droppedAttributesCount": 0,
                            "events": [],
                            "droppedEventsCount": 0
                        }
                    ]
                }
            ]
        }
    ]
}

Both timestamps match the pageload span's start, and parentSpanId points at the pageload span from the earlier request. The entry point and page attributes are copies of what went out on that span, taken from the retained page context rather than read from the live page.

Lifecycle Aggregations

On this page

  • The attributes
  • Why two spans
  • The
  • Effect on page metrics
  • Example

Catch errors and fix slowdowns with Flare, the full-stack application monitoring platform for Laravel, PHP & JavaScript.

  • Platform
  • Error Tracking
  • Performance Monitoring
  • Pricing
  • Support
  • Resources
  • Insights
  • Newsletter
  • Changelog
  • Documentation
  • Affiliate program
  • uptime status badge Service status
  • Terms of use
  • DPA
  • Privacy & cookie Policy
Made in by Spatie logo
Flare