# Verify the connection

> Prove that every runtime of your app reports errors to the right project: verifyIntegration(), a tagged error for any SDK, diagnostic codes and connection problem codes.

“No errors” can mean a healthy app — or an app whose errors never reach Belvador. A **connection check** proves, runtime by runtime, that errors from your app arrive in the right project. It sends one or two marked errors that **never become issues**, and Belvador shows the result on the project's **Connection** tab.

## Why a first issue is not enough

A first issue proves that one error arrived once. It does not prove that:

- the server side reports too, not only the browser;
- uncaught errors (the ones nobody wrapped in `try … catch`) are reported;
- your server code runs inside Belvador's middleware, so errors are seen before an error page swallows them;
- the app still uses an active DSN key after a deploy.

The check covers each of these and keeps the result, so the team can see when it was last proved.

## What the Connection tab shows

Open a project and choose **Connection**. For each runtime the platform needs — **Browser**, **Server**, or both for TanStack Start — you see:

| State | Meaning |
|---|---|
| **Verified** | A check from this runtime arrived and every part it can prove passed |
| **Not verified** | No check has arrived in the last 30 days — the app may or may not be reporting |
| **Problem** (red) | Errors are being rejected or lost right now |
| **Problem** (amber) | Errors arrive, but part of the app is not covered — for example server code outside the middleware |

Open problems are listed first with what to do. Problems close by themselves once the cause is gone; the alert rule **SDK connection problems** sends one message when a problem starts and one when it is fixed — see [Alerts](https://belvador.dev/en/docs/alerts#connection-problem-alerts).

## Run the check with `verifyIntegration()`

From `@belvador/*` 0.2.0, one call runs every check for the runtime it is called from and reads the result back from Belvador. It never throws and never runs by itself — run it once after setup and after deploys that touch error handling, not on every page load.

### Browser

```ts
import { verifyIntegration } from "@belvador/react";

// Run once after setup or deploy (for example behind a debug flag), never on every page load.
// The console shows one expected "Belvador verification (uncaught)" error — that is the test.
const result = await verifyIntegration();
// result.outcome: "verified" | "problem" | "unconfirmed"; the console prints one summary line.
```

The same call exists in `@belvador/browser` and `@belvador/tanstack-start/client`. The browser console shows one expected error, **“Belvador verification (uncaught, expected)”**: the check throws it on purpose to prove that uncaught errors are reported. Pass `{ uncaught: false }` to skip that part; the browser then stays **delivery only**.

### Server (TanStack Start)

```ts title="src/routes/api/verify-belvador.ts"
import { createFileRoute } from "@tanstack/react-router";
import { verifyIntegration } from "@belvador/tanstack-start/server";

// Temporary: open /api/verify-belvador once after deploying, then delete this file.
// It must run inside belvadorRequestMiddleware (src/start.ts) — that is part of the check.
export const Route = createFileRoute("/api/verify-belvador")({
  server: {
    handlers: {
      GET: async () => Response.json(await verifyIntegration()),
    },
  },
});
```

Open `/api/verify-belvador` once after deploying, read the JSON, then delete the file. The check fails with `BLV_HOOK_MISSING` if the route does not run inside `belvadorRequestMiddleware`.

### Cloudflare Workers

```ts title="src/index.ts"
import { verifyIntegration } from "@belvador/cloudflare";

// Inside the fetch handler wrapped by withBelvador. Temporary: remove after the check.
if (new URL(request.url).pathname === "/__belvador/verify") {
  return Response.json(await verifyIntegration());
}
```

### Reading the result

`verifyIntegration()` resolves to an object and prints one line such as:

```text
[belvador] verify server: problem — hook missing (BLV_HOOK_MISSING), delivery confirmed — bv1_3f0c… — https://belvador.dev/en/docs/sdk/verify#blv-hook-missing
```

| Field | Values |
|---|---|
| `outcome` | `verified` — every check confirmed by Belvador; `problem` — something failed (see `checks`); `unconfirmed` — sent, but Belvador could not confirm yet (node offline, read-back off or timed out) |
| `checks[].check` | `init`, `hook`, `delivery`, `uncaught` (browser) |
| `checks[].status` | `confirmed`, `accepted` (sent, not yet read back), `missing` (sent, never arrived), `failed` (with a `code`), `skipped` |
| `verificationId` | `bv1_` + 32 hex characters; the same id appears on the Connection tab |
| `docsUrl` | The section of this page for the first code found |

Options: `timeoutMs` (default 20000 ms, at most 30000), `uncaught` (browser, default `true`), `readback` (default `true`). One call sends at most 2 events and asks Belvador at most 10 times.

## Any SDK: send a tagged error

Every Sentry-compatible SDK can run the check — and so can `@belvador/*` 0.6.1. Send one error with the tag `belvador.verification` set to `bv1_` followed by 32 lowercase hex characters, then read it back.

### Belvador packages before 0.2.0

```ts
import { captureException, flush, withScope } from "@belvador/react";

// One-time connection check: a marked error that never becomes an issue.
const id = "bv1_" + crypto.randomUUID().replaceAll("-", "");
withScope((scope) => {
  scope.setTag("belvador.verification", id);
  captureException(new Error("Belvador verification"));
});
await flush(5000);
console.log("[belvador] verification id:", id);
```

### Node.js

```js
import * as Sentry from "@sentry/node";

// One-time connection check: a marked error that never becomes an issue.
const id = "bv1_" + crypto.randomUUID().replaceAll("-", "");
Sentry.withScope((scope) => {
  scope.setTag("belvador.verification", id);
  Sentry.captureException(new Error("Belvador verification"));
});
await Sentry.flush(5000);
console.log("[belvador] verification id:", id);
```

### Python

```python
import uuid

import sentry_sdk

# One-time connection check: a marked error that never becomes an issue.
verification_id = "bv1_" + uuid.uuid4().hex
with sentry_sdk.new_scope() as scope:
    scope.set_tag("belvador.verification", verification_id)
    sentry_sdk.capture_exception(Exception("Belvador verification"))
sentry_sdk.flush(5)
print("[belvador] verification id:", verification_id)
```

### Read it back

```bash
curl -s "https://ingest.belvador.dev/api/<project-id>/verification/bv1_<id printed by your app>/?sentry_key=<public-key>"
```

Use the project number and public key from the DSN. The answer lists the checks that reached your node:

```json
{ "v": 1, "verification_id": "bv1_…", "node": "connected", "checks": [{ "surface": "server", "check": "delivery", "hook": "unknown" }] }
```

A tagged error proves the delivery path only: a plain Sentry SDK cannot say whether its error hooks run, so the Connection tab shows **delivery only** for that runtime.

## SDK diagnostic codes

The SDK reports these locally — in the browser console with `debug: true`, or to your own `onDiagnostic` callback — at most once a minute per code. They are never sent to Belvador.

```ts
init({
  dsn: "https://<public-key>@ingest.belvador.dev/<project-id>",
  onDiagnostic: (d) => console.warn(d.code, d.count, d.status),
});
// getDiagnostics() returns a snapshot: active, SDK name, DSN host + project + key hint, last send, counts.
```

### `BLV_DSN_MISSING`

`init()` got no DSN, so the SDK is off. Copy the DSN from the project page.

### `BLV_DSN_INVALID`

The DSN is not a Belvador DSN (`https://<32 hex>@host/<project>`). The SDK is off. Copy it again; do not add query strings.

### `BLV_INIT_CONFLICT`

Another error-tracking client already runs in the page, so Belvador did not start. Remove the old `Sentry.init` so errors are not reported twice.

### `BLV_NOT_INITIALIZED`

`captureException` ran while the SDK was off — before `init()`, or after `init()` failed. Call `init()` as early as possible.

### `BLV_SEND_UNAUTHORIZED`

Belvador refused the DSN key (revoked, expired or another project's). The SDK stops sending until the page or Worker reloads. Use an active key from **Project settings → DSN keys**.

### `BLV_SEND_RATE_LIMITED`

The project's rate limit or the monthly quota was reached; events are dropped until the wait (`retryAfterS`) ends. See [intake limits](https://belvador.dev/en/docs/projects-and-dsn#intake-limits).

### `BLV_SEND_REJECTED`

One event was not accepted — usually too large. Keep extra data and attachments small.

### `BLV_SEND_NETWORK`

Belvador could not be reached after retries. Check the network, ad blockers and that your Content-Security-Policy `connect-src` allows `https://ingest.belvador.dev`.

### `BLV_SEND_SERVER`

Belvador answered with a server error after retries. Usually temporary; if it continues, check the status of your node.

### `BLV_QUEUE_FULL`

Too many events waited to be sent at once, so one was dropped. Reduce bursts of repeated errors with `ignoreErrors` or `sampleRate`.

### `BLV_EVENT_TOO_LARGE`

An event or attachment is over the size limit and was dropped before sending — see [SDK configuration](https://belvador.dev/en/docs/sdk/configuration).

### `BLV_HOOK_MISSING`

The check ran in server code that is not inside Belvador's middleware or Worker wrapper, so errors thrown there are not reported automatically. Register `belvadorRequestMiddleware` and `belvadorFunctionMiddleware` last in `src/start.ts` — see [TanStack Start](https://belvador.dev/en/docs/sdk/tanstack-start) — or wrap the Worker with `withBelvador`.

### `BLV_DROPPED_BY_APP`

The verification never left the app: your `beforeSend` returned `null`, `ignoreErrors` matched it, or `sampleRate` below 1 dropped it. Let errors named `BelvadorVerificationError` through.

### `BLV_MONITOR_INVALID_SLUG`

A monitor slug must be lowercase letters, digits, `-` and `_`, at most 64 characters (for example `nightly-invoices`). The job still ran; no check-in was sent for it — see [Monitors](https://belvador.dev/en/docs/monitors/setup).

### `BLV_MONITOR_INVALID_REASON`

A reason passed to `run.skip()`, `run.unknown()` or `captureCheckIn` must be a short code such as `provider_timeout`, not a message. It was left out of the check-in.

### `BLV_MONITOR_CONFIG_INVALID`

The schedule or a setting passed to `withMonitor` is not valid (a crontab without exactly 5 fields, an unknown time zone, a number out of range). The check-in was sent without it, so the monitor's settings did not change.

### `BLV_CHECK_IN_DROPPED`

A check-in was not sent: too many were waiting at once (at most 10 of the 30 queued sends), check-ins were rate limited, or the network failed. Errors are not affected. If it repeats, a monitor may show as missed — check how often the job reports.

### `BLV_TRACE_TARGET_INVALID`

An entry in `tracePropagation.targets` is not a string or a regular expression and was ignored; requests to it get no trace headers.

### `BLV_OTEL_INVALID`

The `otel.api` option is not the `@opentelemetry/api` module (pass `import * as api from "@opentelemetry/api"`, the same copy your OpenTelemetry SDK uses). The bridge stays off; errors are still captured, without the span's ids.

### `BLV_TRACE_OWNER_OTEL`

Both `otel` and `tracePropagation` are set. OpenTelemetry already propagates the trace, so Belvador adds no trace headers of its own and `tracePropagation` is ignored. Remove `tracePropagation` to silence this.

### `BLV_PRIVACY_POLICY_INVALID`

From SDK 0.6.0. `privacyPolicy()` got JSON that is not a valid policy from the project's Privacy page (wrong schema, an unsafe pattern, a bad selector). The app sends without project rules; your node still applies the project's current policy before storing anything. Copy the policy again with "Copy as code".

### `BLV_PREVIEW_ONLY`

From SDK 0.6.0. Preview mode is on (`preview: true` or `BELVADOR_PREVIEW=1`): each envelope is printed with every rule and scrubber applied, and nothing is sent. A verification in preview mode never reaches Belvador. Turn preview off to send.

### `BLV_OFFLINE_UNAVAILABLE`

From SDK 0.6.0. `offline: true` is set, but this browser has no usable IndexedDB (some private windows, a blocked storage policy) or the offline module could not load (a strict CSP without the app's own chunks). Errors are still sent, without an offline queue: one that fails on the network is lost. See [Offline and delivery](https://belvador.dev/en/docs/sdk/offline).

## Connection problem codes

Belvador opens these from what it sees at the edge and from checks. Each one names the project and, when it applies, the runtime.

### `INTEGRATION_KEY_INACTIVE`

Your app still sends with a revoked or expired DSN key; those errors are rejected. Put an active DSN in the app and deploy again.

### `INTEGRATION_ORIGIN_BLOCKED`

Errors come from a website that is not in the project's **Allowed origins**. Add the domain, or empty the list to accept every domain.

### `INTEGRATION_RATE_LIMITED`

Many errors are dropped by the project's rate limit. Raise it in Project settings or reduce repeated errors.

### `INTEGRATION_QUOTA_EXCEEDED`

The workspace's monthly quota is used up; errors are dropped until the next month.

### `INTEGRATION_TOO_LARGE`

Several payloads were too large. Turn off large attachments and keep extra data small.

### `INTEGRATION_INVALID_EVENTS`

The node cannot read some payloads — usually an unsupported SDK or a custom transport.

### `INTEGRATION_BUFFER_LOSS`

The node was offline long enough that waiting errors expired or the waiting room filled up. Bring the node back from the Node page.

### `INTEGRATION_SDK_DROPS`

The SDK in your app reported that it could not send errors (network, CSP or a full queue). Turn on `debug` to see the reason in the console.

### `INTEGRATION_HOOK_MISSING`

A server check ran outside Belvador's middleware — see `BLV_HOOK_MISSING` above.

### `INTEGRATION_UNCAUGHT_MISSING`

A browser check announced an uncaught error that never arrived within 2 minutes: something replaces `window.onerror` or `init()` runs too late.

## Limits

- A check proves the path it ran on, at that moment. Code paths that catch errors themselves still need `captureException`.
- The uncaught browser error is thrown in a `setTimeout` on purpose; you may see it in the console and in browser extensions that watch errors.
- Checks are limited per project (60 an hour) and per key (30 read-backs a minute). Verification events never count as issues and are kept as metadata for 30 days.
- Your node needs version 0.1.17 or newer to recognise checks; older nodes show a banner on the Connection tab.
