# Catch errors in your visitors' browsers

> Add one script tag to your site and JavaScript errors in your visitors' browsers become Lampwick issues. Works in apps built with Lovable, Replit or v0.

A script tag in your site's `<head>` reports JavaScript errors from your visitors' browsers to Lampwick. They show up under **Issues** with a **Browser** label, next to your server's errors. It needs no package and no build step, so it also works in apps made with an AI app builder.

## Requirements

- A Lampwick project. Any member of its team can create the browser key; only owners can rotate it.
- Access to your site's `index.html`, or an AI app builder that can edit it for you.

## Add the snippet

1. Open the project's **Settings** and find **Browser errors**.
2. Click **Get the snippet**. Lampwick creates a public browser key for the project and shows one line.
3. Copy the line into the `<head>` of your site's `index.html`, above the other scripts:

```html
<script src="https://app.lampwick.dev/b.js" data-key="YOUR_BROWSER_KEY" async></script>
```

`YOUR_BROWSER_KEY` is the project's browser key, which starts with `lwpub_`. The snippet on the **Settings** page already has it filled in.

The key is meant to be in your page. It can only send errors to this project and can't read anything; the API, the MCP server and `/ingest` all refuse it.

## Built your app with Lovable, Replit or v0?

Under **Browser errors**, open **Built your app with an AI app builder? Paste this into its chat** and copy the prompt into your builder's chat. It holds your key and reads:

```text
Add the Lampwick error monitoring snippet to the <head> of index.html, exactly as written, before any other script:

<script src="https://app.lampwick.dev/b.js" data-key="YOUR_BROWSER_KEY" async></script>

Don't change the key or move it to an environment variable: it is a public, write-only key that is meant to be in the page. Don't wrap it in a component or install a package for it.
```

## Check that it works

Until the first error arrives, **Browser errors** on the **Settings** page says **No browser errors yet**; after that, **Receiving browser errors** with the time of the last one. To test, open your site, then run this in the browser console:

```js
setTimeout(() => { throw new Error('Lampwick test') })
```

Within a few seconds the error shows under **Issues** with a **Browser** label.

## What it sends

| Captured | How |
|---|---|
| Uncaught errors | `window.onerror`, with the stack trace |
| Unhandled promise rejections | `unhandledrejection`. A rejection that isn't an `Error` sends only its type and a short description, never the whole object. |
| `console.error` calls | Only with `data-console` on the tag |
| Failed `fetch()` calls | Only with `data-fetch`: network errors and HTTP 5xx, as log lines |
| Your own reports | `lampwick.captureException(error)`, `lampwick.captureMessage(text, level)` |

- Page URLs are sent without the query string and fragment, which often hold tokens. Lampwick strips them again on arrival, also from URLs inside messages.
- Errors from cross-origin scripts that the browser reports only as "Script error." are skipped: browsers give no details for them.
- Lampwick groups browser errors into issues itself, by error class, message and the file of the top stack frame. A redeploy with new bundle hashes keeps the grouping.
- Source maps are not supported yet: stack frames show positions in your bundle.

## Options

Set them as attributes on the script tag:

| Attribute | What it does |
|---|---|
| `data-key` | The project's browser key (required). |
| `data-release` | Your app's version, such as `1.4.2`. Shown as **Deploy** on the issue. |
| `data-console` | Also report `console.error` calls. |
| `data-fetch` | Also report failed `fetch()` calls. |

In your code, `lampwick.setUser({ id, email })` ties errors to a signed-in user, and `lampwick.flush()` sends what is queued right away.

## Allowed sites

Under **Allowed sites**, list your site's addresses, one per line: `https://myapp.com`, `https://myapp.lovable.app` or `http://localhost:5173`. `https://*.myapp.com` allows every subdomain of a domain you own. Up to 20 addresses.

With a list, requests from other sites get `403`. Without one, any site can use the key. A list can't stop someone who copies the key into a script of their own; if junk shows up, an owner rotates the key under **Danger zone** (**Rotate browser key**) and you update the snippet.

## Limits

| Limit | Value |
|---|---|
| Per page view | 30 errors, each distinct error once; sent in batches after 2 seconds, or sooner at 10 |
| Per request | 50 events, 64 KiB |
| Per IP address | 120 requests a minute |
| Per key | 600 events a minute, 50,000 a day, 200 new issues a day |
| Per team | Browser errors have their own monthly budget, on top of your plan's events: 50k (Free), 500k (Pro), 2.5M (Team), 10M (Business). At most a fifteenth of it per day. |

When the monthly budget is used up, browser errors are dropped until the 1st (UTC) and **Settings** says so; server monitoring keeps working. Push notifications for browser issues are capped at 5 per project per hour, plus one summary.

## Troubleshooting

| What you see | Cause and fix |
|---|---|
| No browser errors arrive | Check that the tag is in the `<head>` of the page that is served, with the right `data-key`. If you listed **Allowed sites**, the address in the browser's address bar must be on the list. |
| `403` in the browser's network tab | The site isn't on **Allowed sites**. Add its exact address, with `https://`. |
| `429` | A limit above was hit. Requests are accepted again after a minute, or after an hour for the daily and monthly budgets. |
| **Browser errors are paused** on **Settings** | The team used its browser budget for today or this month. See **Billing and usage**. |
| Stack frames point into `index-3f9a2c.js` | Source maps aren't supported yet. |

For errors on your server, use the [Laravel package](/docs/laravel/) or [HTTP ingest](/docs/http-ingest/).
