> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reliantlabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosted dashboards, alerts and issues

> The dashboards forge generates, the default alerts, how errors are grouped into issues, and how source maps and session replay work on Reliant hosting

Once an environment is deployed to Reliant hosting, the platform builds three things from its telemetry without any setup: dashboards cut from your project's shape, a default set of alert rules, and **issues**, which are errors grouped so the same bug is one row however many times it fires. All three are code. Dashboards and alert rules ship inside the release, so promoting a release applies its rules and rolling back restores the previous ones. Issues are the unit that [`forge obs`](https://docs.reliantlabs.io/forge/observability/forge-obs) and the on-call workflow work with.

## Dashboards

Forge generates dashboards from your project's own shape every time you run `forge generate`: an overview of every service, one dashboard per service with a row for every RPC, a frontend dashboard (errors, sessions, page views, failed API calls, web vitals by route, errors by release), a workloads dashboard and a releases dashboard. They are cut into every release, so the console shows the set that belongs to the release an environment is running, and a rollback shows the older set.

The **Dashboards** page lists three kinds, and the difference decides what you can do with each:

| Kind | Where it comes from | Editing |
| - | - | - |
| **Generated** | Forge, from your services and frontends | Read-only. **Save as copy** makes an editable workspace copy. |
| **Repo** | A dashboard file you committed under `deploy/observability/dashboards/` | Read-only in the console, because the file is the source. A repo file named like a generated dashboard replaces it. |
| **Workspace** | Created in the console: blank, from a template, or by importing a forge dashboard file | Editable. Every save is a new version, with history and restore. |

A workspace dashboard is edited in place. **Edit layout** lets you drag panels, set their width in quarters of the grid and remove them; **Add panel** opens a builder where you choose a visualization, a signal, series with an aggregation (count, p50/p95/p99, average, max, sum, distinct), filters, group-by and units, with a live preview over the dashboard's scope and a **Spec** tab holding the panel's JSON. Saving is checked against the version you opened, so two people editing at once cannot silently overwrite each other. When a workspace dashboard is worth keeping, **Promote to code** gives you the forge dashboard file to commit, and from then on it ships with your releases as a repo dashboard.

Every dashboard has environment, service and time variables, compares against the previous period or the previous release, refreshes on an interval, can be shared as a link or shown full screen, and draws a marker for each release and a band wherever an alert was firing. Clicking into a chart drills down to the matching logs, traces or workload. Star a dashboard and it appears under **Dashboards** in the navigation.

### Dashboards as code

Panels are forge's dashboard spec, so a dashboard file in your repo and a panel built in the console are the same thing. Besides time series, numbers, tables and log searches, a panel can be a bar chart, a heatmap, a read-only ClickHouse `sql` query, a product funnel, a list of frontend issues or of browser sessions, and a line or number panel can plot a formula over its series (`A / B * 100`).

To keep a dashboard in your repository, put its JSON in `deploy/observability/dashboards/` and commit it; the next release carries it. The quickest ways to get that JSON:

* **Promote to code** on a workspace dashboard you built in the console.
* Copy a generated file to change it: `cp <project>-overview_gen.json <project>-overview.json`, edit the copy, and forge stops generating the original while yours exists. Never edit a `*_gen.json` directly; the next `forge generate` refuses the drift.
  Metrics you declare in `internal/metrics/metrics.go` get a generated custom-metrics dashboard in every release without a file of your own. `forge obs dashboards check` validates the files and lists what the next release would carry. Agents can load the `observability/dashboards` skill for the full spec.

## Alerts

Alert rules are code. Forge generates a default set for every project, and you tune or disable each one per environment in `deploy/kcl/<env>/main.k`, so promoting a release applies its rules and rolling back restores the previous ones.

| Default | Fires when |
| - | - |
| `ServiceErrorRateHigh` | A service's server-error rate passes 5% over 5 minutes (caller mistakes such as not-found or invalid input are not counted). |
| `ServiceLatencyP95High` | A service's p95 latency passes 1 second over 5 minutes. |
| `ServiceSilent` | A service that normally serves traffic has served nothing for 10 minutes. |
| `FrontendJSErrorSpike` | Browser errors in the last 10 minutes run at more than three times the previous hour's rate. |
| `FrontendLCPPoor` | Largest Contentful Paint at p75 passes 4 seconds. |
| `WorkloadCrashLooping`, `WorkloadOOMKilled`, `WorkloadMemoryNearLimit`, `WorkloadImagePullFailed`, `WorkloadEvicted` | A container keeps restarting, is killed for memory, sits above 90% of its memory limit, cannot pull its image, or is evicted. |
| `NewIssue`, `IssueRegressed`, `IssueSpike` | An error **issue** (see below) is new, a resolved one came back in a later release, or an open one's errors per hour reach five times its rate the day before (and at least 20). Each fires once per issue and transition, with the issue's id, service, title and a console link in its labels; it is how errors reach Slack, PagerDuty and [Autopilot on-call](https://docs.reliantlabs.io/guides/autopilot.md#alerts-and-errors). |
| `DeployFailed` | A promoted release fails to converge and stays failed for 5 minutes. |
| `ObservabilityQuotaOver100`, `ObservabilityQuotaOver150` | Your organization passes its monthly observability allowance. |

Each default has thresholds you can change, and your own rules are a ClickHouse `SELECT` where every returned row is a firing alert:

```python theme={null}
observability = forge.Observability {
    alerts = forge.Alerts {
        defaults = {
            "error-rate": forge.AlertDefault {params = {threshold = 0.02}}
            "service-silent": forge.AlertDefault {enabled = False}
        }
        routes = [
            forge.AlertRoute {match = {severity = "critical"}, receiver = "oncall", continue = True}
            forge.AlertRoute {match = {}, receiver = "slack"}
        ]
    }
}
```

`forge env render` refuses a malformed rule before anything ships, and `forge obs alerts check` evaluates your rules against the local stack. Agents can load the `observability/alerts` skill for the rule format and every default's parameters.

A route sends alerts to a **receiver**, and a receiver is only a name in your code. What the name points at, a Slack channel, an email list, a webhook or a PagerDuty service, is set in **Observability → Alerts → Receivers**, because a webhook URL or a routing key does not belong in a repository. The built-in **In-app** receiver needs no setup. With no routes declared, alerts go to in-app notifications and then to a receiver named `slack`, and never to a pager until you route them there. The Receivers tab calls out a route that names a receiver you have not set up. Creating or changing receivers needs the organization admin role; their secrets are write-only.

The same page shows what is **Firing**, the alert **History**, the declared **Rules** and **Silences**, which mute matching alerts for a while without changing code.

## Frontend errors, issues and source maps

The browser SDK reports an error with a stack from your minified bundle, which changes with every build. Forge builds every deployed frontend with source maps, moves the maps out of the files it serves, and files them privately with the release, so your users never download them. The platform resolves each new error stack through the maps of the build that produced it, so the same bug is recognised as one **issue** across releases, and tracks each issue per environment as new, unresolved or regressed. The `issues` panel on the frontend dashboard lists errors grouped by stack, most frequent first, with the page they happened on, the release, how often and since when, and a sample trace.

Your frontend's `forge.Frontend` declaration can tune this:

```python theme={null}
telemetry = forge.BrowserTelemetry {
    console_capture = False                                  # do not ship console output
    errors = forge.FrontendErrors {ignore = ["AbortError", "re:^Failed to fetch"]}
    funnels = [forge.Funnel {
        id = "onboarding"
        steps = ["signup_started", "workspace_created", "first_chat"]
        window = "7d"
        by = "user"
    }]
}
```

An error matching `ignore` still reaches the store but is never raised as an issue. `sourcemaps = "public"` serves the maps instead of filing them, and `"off"` drops them. A **funnel** names events your frontend records with `track("signup_started")` from `@reliantlabs/forge-web-runtime`; forge draws each declared funnel as a panel on the frontend dashboard.

## Session replay

Session replay records what a user's browser displayed, so it is off unless you turn it on in two places: build the frontend with `VITE_OTEL_REPLAY=true` (Vite) or `NEXT_PUBLIC_OTEL_REPLAY=true` (Next.js), and opt the environment in with `signals = forge.ObservabilitySignals {replay = True}`. Until both are set, the platform drops replay data rather than storing it. When replay is on, all text and every input is masked and canvases are not recorded, and there is no setting that records unmasked. `replay_sample_rate` in `forge.BrowserTelemetry` records only a share of sessions. The `sessions` panel marks the sessions that have a recording.

## How errors are grouped

An **issue** is one distinct bug. Server errors are grouped by a fingerprint that is stable across releases, so an issue keeps its identity when a line number moves. The fingerprint is built from where the error was raised (the function), the RPC it happened in, the message with its variable parts masked out, and the kind of failure it carries (the error's Go type or its SQLSTATE). It never uses a line number, because lines move between releases and would split one issue into many. An error that carries an explicit group key uses that instead.

The kind of failure matters. When a generic `Transaction failed` message covers UTF-8 corruption, serialization failures and timeouts, folding the SQLSTATE into the fingerprint keeps them as separate issues, each of which has a different fix. Only errors that count as the server's fault are grouped (see [error levels](https://docs.reliantlabs.io/forge/observability/signals.md#who-has-to-act-error-levels)); a caller's `InvalidArgument` is never an issue. Browser errors are grouped by their stack after source maps resolve it.

Each issue tracks its state per environment: **new**, **unresolved**, **resolved** or **regressed**. `NewIssue`, `IssueRegressed` and `IssueSpike` alerts fire on those transitions, once each. `forge obs issues` and `forge obs issue <id>` read the same issues from the terminal, locally and hosted; see [Querying with forge obs](https://docs.reliantlabs.io/forge/observability/forge-obs.md#issues-traces-logs-and-rows).

## Related topics

[What every boundary records](https://docs.reliantlabs.io/forge/observability/signals.md) is where these signals come from. [Where the telemetry goes](https://docs.reliantlabs.io/forge/observability/where-it-goes.md) covers retention, usage and opting out. Autopilot's on-call workflow reacts to these alerts and issues; see [Autopilot](https://docs.reliantlabs.io/guides/autopilot.md#alerts-and-errors).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.