Frontend monitoring

From the click in the browser to the query behind it.

The browser SDK reports uncaught errors, unhandled rejections and each fetch and XMLHttpRequest call from your users' browsers. Calls to your own origins carry a traceparent header, so the trace continues in your API. Add web vitals, and a Vite plugin that uploads source maps so minified stacks read like your code.

Browser errors
Uncaught errors and rejections as issues
Linked traces
traceparent continues into your API
Web vitals
LCP, INP, CLS, FCP and TTFB per route
Source maps
Uploaded by the Vite plugin at build time
#stack-traces

Minified stacks, mapped back to your source

Upload the source maps of a release and each browser frame is mapped back to your file, function and line when the error arrives, before it is grouped. Browser errors then group on your real function names.

  • Each mapped frame keeps the minified location it came from
  • A frame left minified says why: no release, no matching map, or no mapping at that position
  • Errors carry the page URL, the release and the user you set with setUser
#setup

One client for errors, requests and web vitals

Call initUnhandled once in your entry file. With no options it records network spans and uncaught errors. List your API origin so its requests continue the trace, and add webVitals() for page speed.

  • Each fetch and XMLHttpRequest call becomes a span with its method, URL and status
  • A network failure or a 5xx response marks the span as an error
  • Query strings are dropped and token-like path segments become REDACTED
src/unhandled.ts
import { errors, initUnhandled, network, webVitals } from '@unhandled/browser'

export const unhandled = initUnhandled({
  dsn: import.meta.env.VITE_UNHANDLED_DSN,
  environment: 'production',
  integrations: [
    network({
      tracePropagationTargets: [
        window.location.origin,
        'https://api.shop.test',
      ],
    }),
    errors(),
    webVitals(),
  ],
})
#web-vitals

Web vitals from real visits

webVitals() reports LCP, INP, CLS, FCP and TTFB as browser.web_vital log records with the value, its rating and the route. Search them in Logs like any other record, for example browser.web_vital.name=INP,LCP.

  • Where it can, a record names the element the attribution build blames
  • Routes come from your route function or an unhandled-route meta tag
  • Sampling is decided once per page load, and the integration adds about 4 KB gzipped
#source-maps

Releases and source maps from the build

The Vite plugin reads the release from your hosting platform or CI and injects it into the bundle. With sourcemaps set, it builds hidden source maps, uploads them under that release and deletes them, so they never ship.

  • Reads the commit from Vercel, Render, Railway, GitHub Actions, Workers Builds and others, or from .git
  • Uploads use a project deploy key from UNHANDLED_API_KEY, never the DSN in your bundle
  • A failed upload never fails the build
vite.config.ts
import { defineConfig } from 'vite'
import { unhandled } from '@unhandled/browser/vite'

export default defineConfig({
  plugins: [unhandled({ sourcemaps: { upload: true } })],
})
#issues

Browser errors are issues like any other

Uncaught errors and unhandled rejections land in Issues next to your backend exceptions, grouped once per bug, with the releases and users they reached.

  • A burst limit caps error loops: 10 reports per message, then one every 10 seconds
  • ignoreErrors drops errors by type, message or file, and built-in filters drop extension and ad script noise
  • captureException reports the errors you catch, with tags, extra data and a user
#faq

Questions

Try it on your own stack.

Every feature is on every plan, priced by the telemetry you send.