Docs
Search docs

Report real-user monitoring samples

POST/api/rum

ingestRum

Accepts a batch of browser-measured samples from the web app — Web Vitals (LCP, INP, CLS, TTFB, FCP), long animation frames, and the API calls behind a page with their cache outcome — and records each as one Prometheus histogram observation. Nothing is stored and no identity is read; anonymous callers are accepted. A request whose `Origin` is another site is answered `204` and records nothing. `route_id` and `api_route` are validated against closed sets (the app's route ids and this server's API path templates); a value outside them is recorded as `unknown`, never echoed into a label. A sample with a negative, non-finite or implausibly large value is dropped. The body is parsed as JSON whatever its `Content-Type`, so a `navigator.sendBeacon` with a plain-text blob works. At most 100 samples per batch (more is `400`); the body is capped at 16 KiB (`413`). Rate limited per IP.

Authentication optional — this operation also accepts anonymous requests. When authenticating, use a bearer token.

curl

curl -X POST 'https://neosource.dev/api/rum' \
  -H 'Content-Type: application/json' \
  -d '{"samples":[]}'

fetch

fetch("https://neosource.dev/api/rum", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"samples":[]}),
});

Request bodyrequired

application/json

RumBatchRequest

object

Body of `POST /api/rum`: up to [`RUM_MAX_SAMPLES`] samples.

samplesrequired

array

items

RumSample

object

One measurement from one page view.

api_route

string | null

`api` only: the API path TEMPLATE the call was served by. A `String` for the same reason as `route_id`; a template the server does not route is recorded as `unknown`. Absent on a query-cache hit, which has no request to attribute.

example"/api/repos/{owner}/{repo}"

cache
one of
metricrequired
RumMetric
route_idrequired

string

The app's route id for the page (`/[owner=owner]/[repo]`), never a concrete path. Deliberately a `String`: the closed set is generated from the app's route tree, and an id the server does not know is recorded as `unknown` rather than refused.

example"/[owner=owner]/[repo]"

server_seconds

number | nulldouble

`api` only: the server's own `app` duration from the response's `Server-Timing`, in seconds, so `value − server_seconds` is the network and queueing share.

valuerequired

numberdouble

Seconds for every metric but `cls`, which is unitless. Negative or non-finite values are dropped.

Responses

204Batch recorded

413The body exceeded the route's 16 KiB cap.

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 429Rate limited — retry after the interval in the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

RumCacheOutcome

string

Where an API call's answer came from, as the page saw it.

"prefetch""query""network"

RumMetric

string

What one RUM sample measures.

"lcp""inp""cls""ttfb""fcp""loaf""api"