# Frontend Error Reporting

## TL;DR — zero config needed

**The starter app deployed by `create_project` already has frontend error reporting active.** It wraps the handler with `withErrorReporting`, which auto-injects a `<script>` into every HTML response. No listeners to write, no client code to add.

If you wrote a custom handler, add one line:

```typescript
import { withErrorReporting } from "./.onvibe/helpers.ts";

async function handler(req: Request): Promise<Response> {
  return new Response("<!DOCTYPE html>...", {
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

export default withErrorReporting(handler);  // ← this is all you need
```

`./.onvibe/helpers.ts` is injected by the platform at deploy time — do **not** include it in your source files.

---

## How withErrorReporting works

- All requests pass through to your handler unchanged.
- For responses with `Content-Type: text/html`, the HOF injects a `<script>` before `</head>` (or prepended if missing) that:
  - Hooks `window.onerror` — catches uncaught synchronous exceptions and script errors.
  - Hooks `window.addEventListener('unhandledrejection', ...)` — catches unhandled Promise rejections.
  - POSTs each error to `/.onvibe/exceptions` asynchronously (fire-and-forget, never blocks the UI).
- Non-HTML responses (JSON, images, plain text) are passed through untouched.

---

## The /.onvibe/exceptions endpoint

Every deployed app exposes this route at the same origin:

```
POST /.onvibe/exceptions
Content-Type: application/json
```

The runner intercepts `POST /.onvibe/exceptions` before passing control to your handler — no auth required from the browser (same-origin only). `GET` falls through to your handler.

### Accepted payload fields

| Field | Type | Description |
|---|---|---|
| `message` | string | Error message (required) |
| `stack` | string | Full stack trace as returned by `err.stack` |
| `url` | string | `window.location.href` at the time of the error |
| `timestamp` | string | ISO 8601 timestamp |

> **Note:** Fields such as `source`, `line`, `col`, and `kind` are accepted but not currently persisted separately. Include them in the `stack` string if you need them (e.g. `src + ':' + line + ':' + col` when `err.stack` is unavailable).

The endpoint always returns `204 No Content`.

---

## Viewing captured errors

Use `get_exceptions`. Each row includes a `type` field:

| type | Source |
|---|---|
| `backend` | Unhandled server-side exception caught by the runner |
| `frontend` | Browser-side JS error reported via `/.onvibe/exceptions` |

```json
[
  {
    "type": "frontend",
    "message": "Cannot read properties of undefined (reading 'id')",
    "stack": "TypeError: Cannot read properties of undefined...\n    at handler (https://myapp.onvibe.run/:12:5)",
    "request_url": "https://myapp.onvibe.run/dashboard",
    "occurred_at": "2025-01-01T12:00:00Z"
  }
]
```

---

## Known browser limitations

- **Errors inside setTimeout / setInterval**: The stack trace loses the original call site. Only the frame where the `throw` happens is captured, not the chain that scheduled it. For richer stacks, throw closer to the origin or use `async/await` instead of callbacks (`unhandledrejection` captures the full async chain).
- **Cross-origin scripts**: Errors from scripts loaded from a different origin arrive as `"Script error."` with no stack. Add `crossorigin="anonymous"` to the `<script>` tag and ensure the script host sends `Access-Control-Allow-Origin: *`.
- **Stack format varies by browser**: Chrome uses `at func (url:line:col)`; Firefox uses `func/<@url:line:col`. The raw stack string is stored as-is.

---

## Manual integration (advanced)

Only needed if you cannot use `withErrorReporting` (e.g. a pre-rendered static site with no server handler):

```javascript
window.onerror = function(msg, src, line, col, err) {
  fetch('/.onvibe/exceptions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: String(msg),
      stack: err ? err.stack : (src + ':' + line + ':' + col),
      url: window.location.href,
      timestamp: new Date().toISOString(),
    }),
  }).catch(function(){});
  return false;
};

window.addEventListener('unhandledrejection', function(e) {
  var err = e.reason;
  fetch('/.onvibe/exceptions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: err instanceof Error ? err.message : String(err),
      stack: err instanceof Error ? err.stack : undefined,
      url: window.location.href,
      timestamp: new Date().toISOString(),
    }),
  }).catch(function(){});
});
```

---

## Constraints

- Errors are stored per-project, capped at the last 10 entries.
- No CORS headers — the endpoint is for same-origin use only.
