Performance

Browser performance

Measure page loads, in-app navigations, Core Web Vitals and API calls as your users experience them, with @errorgap/browser.

Server timings say how long your backend took. Browser timings say what the visit cost the person on the other end: network, rendering, and every API call the page made. They are kept apart from server transactions and shown under Performance → Browser.

Enable it

Browser performance is part of @errorgap/browser 0.2 and later, and is off until you turn it on. It uses the same project key as error reporting.

import { Errorgap } from "@errorgap/browser";

Errorgap.init({
  endpoint:    "https://errorgap.example.com",
  projectSlug: "your-project",
  apiKey:      "your-project-api-key",
  release:     __APP_VERSION__,
  performance: true,          // or { sampleRate: 0.25, routeName: () => ... }
});

What is measured

  • Page loads — time to the load event, time to first byte, DOM ready and first contentful paint.
  • Core Web Vitals — LCP, INP and CLS, measured with Google's web-vitals library and reported when the page is hidden, so they cover the whole visit.
  • In-app navigations — route changes made with pushState or the back button, timed until the next painted frame and until the new route's requests finish.
  • API calls — every fetch and XMLHttpRequest: method, URL, status (0 when no response arrived) and duration, network included.

Route names

Timings are grouped by route. By default the route is the page's path, and Errorgap replaces id-like segments — numbers, UUIDs, long hashes — with :id, so /orders/123 and /orders/456 are one row. For names that match your router exactly, pass routeName:

performance: {
  // React Router, TanStack Router, Vue Router… whatever knows the matched route.
  routeName: () => currentRoute()?.path,   // e.g. "/projects/:slug/errors"
},

Reading the Browser view

Every timing is a 75th percentile, which is how Web Vitals are judged: three visits in four were at least this fast. Each vital is rated by Google's thresholds:

  • LCP — good at 2.5 s or less, poor above 4 s.
  • INP — good at 200 ms or less, poor above 500 ms.
  • CLS — good at 0.1 or less, poor above 0.25.
  • TTFB — good at 800 ms or less, poor above 1.8 s.

The bar under each vital shows the share of visits in each band. The Pages table splits full loads from in-app navigations; the API calls table shows latency and failure rates as the browser saw them, which includes network time your server never sees.

Linking API calls to server traces

With @errorgap/browser 0.3 and later, each timed API call carries an x-errorgap-trace header holding a random id. A server SDK that records it ties the call to the server transaction that answered it: the API calls table gains a Server column linking each endpoint's slowest traced call to its server trace, and that trace shows the browser's timing next to the server's. The difference is network and queueing time. The two projects only need to be in the same organization.

The header goes to your own origin by default. To send it to an API on another origin, list it, and allow the header in that API's CORS configuration:

performance: {
  tracePropagationTargets: ["https://api.example.com", /^https://[a-z]+.example.com/],
},

Server SDKs that record the header: Ruby (Rack) 0.7, PHP 0.5, Laravel 0.4, Node.js (Express 0.2, Fastify 0.3), Python (WSGI/Flask 0.2, Django and FastAPI 0.3), Next.js 0.5 (withErrorgap route handlers, withErrorgapAction, withErrorgapPage), Nuxt 0.4 (apm: true), Bun and Deno 0.4 (withErrorgap around the serve() handler), Go (stdhttp) 0.4, Java (Spring) 0.4, .NET (ASP.NET Core) 0.4, Rust 0.5 (Transaction::trace_id) and Elixir 0.5 (plug Errorgap.Plug, apm: true). Only well-formed ids are accepted.

Mobile apps link their calls the same way: React Native, Swift, Android and Flutter 0.4 add spans.traceCall(label, fn), which hands your HTTP client the header to send and records the call as a span. The app's trace then lists each traced call with a link to the server request that answered it, and that server trace shows which app screen called it.

Privacy and volume

  • Query strings and fragments are never sent. Same-origin calls are recorded as a path; other origins keep their host.
  • Timings are batched every 10 seconds, and the last batch is sent with navigator.sendBeacon when the page is hidden.
  • Use sampleRate to measure a share of page loads on busy sites. Browser timings are kept for 30 days (ERRORGAP_BROWSER_RETENTION_DAYS on self-hosted installs).