Web Vitals
Hosted Agent Analytics stores cwv_* on page_view. Self-hosted core stores the same keys on a $web_vitals event. Hosted does not store $web_vitals.
POST /vitals and GET /vitals are hosted reads. The event is fixed to page_view.
Collection
Section titled “Collection”data-track-vitals="true" turns collection on. data-track-vitals="false" turns it off. Default install snippets include data-track-vitals="true". A snippet that omits the attribute stays off. Existing opt-in installs keep the same flag.
On hosted, each flush is merged into the latest page_view for that session and path. A later flush overwrites those keys. The path sent with the vitals is the path captured at the start of that page.
data-track-performance still writes perf_ttfb on page_view as responseStart - requestStart. That is not cwv_ttfb.
Metrics
Section titled “Metrics”The headline is the 75th percentile (p75) of page loads. The method is nearest-rank: sort the values ascending, break ties by event id, and take rank ceil(0.75 * n). Rank 1 is the smallest value. The result is an observed value.
| Metric | Property | Unit | Definition |
|---|---|---|---|
| LCP | cwv_lcp |
ms, integer | Largest Contentful Paint. Omitted when the page was hidden before the first paint. |
| INP | cwv_inp |
ms, integer | Interaction to Next Paint. Event entries are grouped by interactionId. Each interaction contributes its max duration. INP is the worst interaction after ignoring one highest interaction per 50 interactions. |
| CLS | cwv_cls |
unitless, 3 decimal places | Cumulative Layout Shift. Largest session window: gap under 1 second, window at most 5 seconds, shifts with hadRecentInput excluded. |
| FCP | cwv_fcp |
ms, integer | First Contentful Paint. Omitted when the page was hidden before the first paint. |
| TTFB | cwv_ttfb |
ms, integer | Time to First Byte. Navigation responseStart - max(activationStart, 0), measured from navigation start. |
Prerendered pages are not tracked.
Core Web Vitals are LCP, INP, and CLS. FCP and TTFB are the other two metrics on this read. Definitions and thresholds follow Web Vitals.
Thresholds and rating
Section titled “Thresholds and rating”A value is good when it is <= good_max, poor when it is > poor_min, and needs-improvement otherwise. The p75 rating uses that rule on the p75. share uses the same rule on each page load. Share fractions are from 0 to 1, with 3 decimal places. The share keys are good, needs_improvement, and poor. The rating string uses a hyphen (needs-improvement). The share key uses an underscore (needs_improvement).
| Metric | Unit | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP | ms | <= 2500 | <= 4000 | > 4000 |
| INP | ms | <= 200 | <= 500 | > 500 |
| CLS | unitless | <= 0.1 | <= 0.25 | > 0.25 |
| FCP | ms | <= 1800 | <= 3000 | > 3000 |
| TTFB | ms | <= 800 | <= 1800 | > 1800 |
cwv_pass uses only LCP, INP, and CLS. It is true when the LCP, INP, and CLS p75 values are all good. When INP n is 0, pass or fail uses LCP and CLS only. cwv_pass is null when LCP n is 0 or CLS n is 0.
Browser support
Section titled “Browser support”CLS and INP are collected in Chromium only. On other browsers the key is absent. It is not stored as 0.
LCP, FCP, and TTFB are written when the browser reports the entry. A browser that does not report the entry omits the key. An omitted key is not 0.
n and skipped
Section titled “n and skipped”n is the number of page_view rows in the group that included that metric. skipped is the number of page_view rows in the group that did not.
A missing vital is skipped, not a zero. Route-change views can lack LCP and FCP. Non-Chromium browsers lack CLS and INP. A page with no interaction lacks INP. There is no minimum n on this read.
Attribution
Section titled “Attribution”When the vitals flag is on, the tracker can send three target properties:
| Property | Selector |
|---|---|
cwv_lcp_target |
LCP element |
cwv_inp_target |
INP interaction target |
cwv_cls_target |
CLS largest shift target |
Each value is a CSS selector, trimmed, at most 200 characters. The fields contain selectors only: no text content and no URLs.
top_poor_targets lists up to 5 selectors by count among loads rated poor for that metric. Each entry is { target, count }. FCP and TTFB have no target field, so top_poor_targets is [].
POST /vitals and GET /vitals
Section titled “POST /vitals and GET /vitals”Pro. A free account receives 403 with error PRO_REQUIRED. That 403 is not metered. One successful response counts as one metered read. The body on 403 is the standard error object: error, message, and payment when the caller can see billing.
POST /vitals JSON body, or the same fields as query parameters on GET /vitals:
| Field | Required | Values |
|---|---|---|
project |
yes | Project name |
days |
no | Integer day window |
date_from |
no | YYYY-MM-DD |
date_to |
no | YYYY-MM-DD |
group_by |
no | device or path |
limit |
no | Default 10, maximum 20. Used when group_by is path. |
The event is page_view. /query and /compare are unchanged.
group_by omitted: one row, group is null. group_by device: group is mobile, tablet, or desktop. group_by path: the top limit paths by n, sorted by worst LCP p75. group is the pathname.
Percentiles are nearest-rank. The sample is capped at 200,000 events. When the cap is hit, the response is still 200, approximate is true, and caps is present. Otherwise approximate is false and caps is omitted.
{ "project": "my-site", "date_from": "2026-11-06", "date_to": "2026-11-12", "event": "page_view", "group_by": null, "percentile": "p75", "percentile_method": "nearest_rank", "approximate": false, "thresholds": { "lcp": { "unit": "ms", "good_max": 2500, "poor_min": 4000 }, "inp": { "unit": "ms", "good_max": 200, "poor_min": 500 }, "cls": { "unit": "unitless", "good_max": 0.1, "poor_min": 0.25 }, "fcp": { "unit": "ms", "good_max": 1800, "poor_min": 3000 }, "ttfb": { "unit": "ms", "good_max": 800, "poor_min": 1800 } }, "rows": [ { "group": null, "cwv_pass": true, "metrics": { "lcp": { "p75": 2100, "rating": "good", "n": 400, "skipped": 20, "share": { "good": 0.8, "needs_improvement": 0.15, "poor": 0.05 }, "top_poor_targets": [{ "target": "img.hero", "count": 4 }] }, "inp": { "p75": 180, "rating": "good", "n": 360, "skipped": 60, "share": { "good": 0.75, "needs_improvement": 0.2, "poor": 0.05 }, "top_poor_targets": [{ "target": "button.buy", "count": 3 }] }, "cls": { "p75": 0.08, "rating": "good", "n": 390, "skipped": 30, "share": { "good": 0.82, "needs_improvement": 0.12, "poor": 0.06 }, "top_poor_targets": [{ "target": "#main > .banner", "count": 2 }] }, "fcp": { "p75": 1600, "rating": "good", "n": 400, "skipped": 20, "share": { "good": 0.84, "needs_improvement": 0.12, "poor": 0.04 }, "top_poor_targets": [] }, "ttfb": { "p75": 700, "rating": "good", "n": 400, "skipped": 20, "share": { "good": 0.78, "needs_improvement": 0.16, "poor": 0.06 }, "top_poor_targets": [] } } } ]}p75 and rating are null when n is 0. rating is good, needs-improvement, poor, or null.
The hosted MCP tool is web_vitals. Arguments match /vitals: project, days, date_from, date_to, group_by, and limit. The tool description includes the thresholds. p75 is the headline. The text result prints, for each metric, the p75 with its unit, the rating, the good / needs-improvement / poor share, and n.
query and compare still accept cwv_* as numeric properties. On hosted, the event is page_view.
vitals is in @agent-analytics/cli@0.5.41. It calls the hosted /vitals read.
npx --yes @agent-analytics/cli@0.5.41 vitals <project> [--days N] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--by device|path] [--limit N] [--json]--from and --to are date_from and date_to. --by is group_by. --limit defaults to 10 and the maximum is 20. --json prints the API body.
CLS and INP on 2026-11-13
Section titled “CLS and INP on 2026-11-13”On 2026-11-13, CLS and INP changed in place (tracker 0.2.5).
Before that date, CLS was the sum of layout-shift values without hadRecentInput, including a stored 0 when another vital was present and layout-shift was unsupported. INP was a percentile of every event entry with an interactionId, so one interaction could contribute several durations.
From 2026-11-13, CLS is the largest session window, and the key is omitted when layout-shift is unsupported. INP is the worst interaction after grouping by interactionId, ignoring one highest interaction per 50. cwv_fcp, cwv_ttfb, and the three target keys start on that date.
A window that crosses 2026-11-13 mixes the two definitions. n can differ by metric across that date. See Release notes.
