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
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
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
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 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
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
import { defineConfig } from 'vite'
import { unhandled } from '@unhandled/browser/vite'
export default defineConfig({
plugins: [unhandled({ sourcemaps: { upload: true } })],
})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
Works with
Frameworks and build tools
- Browser SDKSingle-page apps and server-rendered sites.
- Vite pluginDetects the release and uploads source maps at build time.
- AstroThe SDK in your layout, source maps uploaded from dist/client.
- React NativeJS errors with Hermes source maps, queued on disk while offline.
- Next.jsServer errors and request traces from instrumentation.ts.
- Unhandled CLIUpload source maps and mark deployments from any CI.
Questions
Try it on your own stack.
Every feature is on every plan, priced by the telemetry you send.