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

# Environments — filter traces, evaluations, and guardrails by deployment stage

> Every project ships with dev, staging, and prod. Add your own, switch between them from the header, and filter any view down to just one.

An **environment** is a deployment stage — `dev`, `staging`, `prod`, or whatever your pipeline actually looks like — that scopes traces, evaluations, guardrails, incidents, and metrics so you can look at one stage of your pipeline without the others in the way. It's a first-class row in the project, not a free-text tag: it has a slug, a display name, a rank (for ordering), a production flag, and an optional monthly event quota field (see [Monthly quota](#monthly-quota-not-yet-enforced) below).

<Steps>
  <Step title="Switch environments from the project header">
    Every project page has an environment switcher next to the project name. It defaults to **All environments** — pick one to scope the current page's data to just that environment.
  </Step>

  <Step title="Manage environments">
    Go to **Settings → Environments** to create, rename, reorder, or delete environments, and to mark one as production.
  </Step>

  <Step title="Filter the API directly (optional)">
    Every endpoint the switcher drives also accepts an `environment` query parameter directly — see [Filtering the API](#filtering-the-api) below.
  </Step>
</Steps>

## The three defaults, and custom ones

Every project is seeded with three environments on creation:

| Slug      | Display name | Rank | Production |
| --------- | ------------ | ---- | ---------- |
| `dev`     | Development  | 0    | No         |
| `staging` | Staging      | 10   | No         |
| `prod`    | Production   | 20   | Yes        |

These three are a starting set, not an enum. Slugs are free-form beyond them — `qa`, `uat`, `prod-eu`, `canary` are all valid, as long as the slug is lowercase alphanumeric with hyphens (max 50 characters) and unique within the project. Create one from **Settings → Environments → New environment**, or `POST /v1/projects/:id/environments`.

Creating, renaming, reordering, or deleting an environment requires the `environments:manage` permission (owner/admin/editor); viewing the list only needs `environments:read`.

### Deleting an environment

An environment can carry guardrail configs, alert rules, SLA policies, health thresholds, agent lifecycle gates, prompt deployments, and incidents. Deleting it does **not** silently orphan those — the delete is blocked with a `409` naming exactly what's still attached (e.g. `"3 guardrail config(s), 1 alert rule(s)"`), so you know what to move or delete first before the environment itself can go.

## The switcher and the "omitted means all" rule

The environment switcher writes its selection into the URL as `?env=<slug>` — so a filtered link is shareable and survives a page reload. Clearing the selection (**All environments**) removes the param entirely rather than writing a default back into the URL.

This matters for the API too: **omitting the `environment` parameter means all environments**, on every endpoint that accepts it. This is deliberate — every existing integration and saved link that predates this feature keeps working unfiltered, exactly as before. You only see a single environment's data when you (or the switcher) explicitly ask for one.

## Filtering the API

The `environment` query parameter is accepted on 16 read endpoints across traces, evaluations, guardrails, incidents, and metrics — the same set of pages the switcher covers. Pass a slug:

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl "$ZESPAN_API_URL/v1/projects/$PROJECT_ID/traces?environment=staging" \
  -H "x-api-key: $ZESPAN_API_KEY"
```

An unrecognized slug returns `400`:

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{ "message": "Unknown environment \"qa-2\"" }
```

This is the opposite of what happens at ingest time — see [Alias mapping and the ingest asymmetry](#alias-mapping-and-the-ingest-asymmetry) below for why reads reject what ingest would silently accept.

## Alias mapping and the ingest asymmetry

The `environment` column in the underlying event store predates this feature — it's been populated by the SDK's free-text `environment` option (or OTel's `deployment.environment` attribute) since long before environments were a project-level entity. That means historical data uses whatever string a client happened to send, most commonly `production`, `development`, `staging`, and `test`, not the seeded `prod`/`staging`/`dev` slugs.

To keep old and new data queryable together, a shared alias map normalizes the common cases both ways:

| Free-text value | Resolves to slug            |
| --------------- | --------------------------- |
| `production`    | `prod`                      |
| `development`   | `dev`                       |
| `staging`       | `staging` (already matches) |

Filtering `?environment=prod` matches rows stored as either `prod` or `production` — you never have to know or care which era a given row came from.

**Ingest and reads handle an unrecognized value differently, on purpose:**

* **Reads reject.** `?environment=qa-2` on a project with no `qa-2` environment returns `400`. A read is a request for a specific view; failing loudly beats silently returning nothing or (worse) everything.
* **Ingest keeps and flags.** A trace arriving with `environment: "load-test-3"` that doesn't match any known slug or alias is still stored — under `load-test-3`, unmodified — with a warning logged (throttled to once per project per hour, so a typo'd environment string on every span doesn't flood the logs). Losing telemetry because of an unfamiliar environment string would be a far worse outcome than an imprecise dimension on an otherwise-good trace.

If you see an unexpected environment value showing up, it usually means a client is sending something that isn't yet one of the project's environments — create it (**Settings → Environments → New environment**) with a matching slug and future events will resolve against it directly, no alias needed.

## Monthly quota (not yet enforced)

Each environment has an optional `monthlyEventQuota` field, settable from the same create/edit form and returned by the API. **Nothing currently enforces it** — it does not throttle ingest or trigger an alert on its own. The field exists so environment-level quota enforcement, when it ships, doesn't require a second schema change. Don't rely on it as a working limit today; project-level quotas (see [Billing](/account/billing)) are the ones actually enforced.

## Next steps

* [Traces](/dashboard/traces) — the primary view the environment switcher scopes
* [Guardrails](/dashboard/guardrails) and [Alerts](/dashboard/alerts) — can be scoped to a specific environment via the same `environmentId` relation the delete-block check above walks
* [Environments guide](/guides/environments) — when to use environment filtering within one project versus separate projects entirely
