# Compare environments

> Send captured requests to two environments — or compare what production returned with what staging returns now — and see per request whether the status, the fields, their types or their values differ, with timestamps and request IDs ignored. Mirror live traffic to a second host with Map Remote's mirror mode. MCP and CI included.

Source: https://docs.subcloak.com/cloak-proxy/testing/compare-environments/

Added in Cloak Proxy 0.4.0.

"Does staging behave like production?" usually means two exports and a diff. **Compare Environments** answers it from the traffic you've already captured: each request is sent to the other environment (or to both), and the two answers are compared field by field — status, fields only on one side, types and values — with the fields that change on every call left out.

It has two modes:

- **Run Against Both** replays captured requests against two environments, or against one and the responses you captured.
- **Mirror Live Traffic** copies live requests to a second host as the app runs, with a [Map Remote](https://docs.subcloak.com/cloak-proxy/tools/map-remote/) rule in mirror mode, and compares each copy's answer with the original's.

*[Screenshot: The Compare Environments window comparing Production (the captured responses) with Staging (https, api.staging.shopapp.example, one header replaced). The list shows GET /v2/recommendations with 200 in production and 503 in staging and GET /v2/stores/{id} with 200 and 404, both Status differs; GET /v2/products and GET /v2/users/me as Shape differs; GET /v2/addresses and GET /v2/config as Values differ; GET /v2/cart, /v2/orders/{id} and /v2/products/{id} as Same; and four POST requests not sent. The selected GET /v2/products shows Shape differs, 89% alike, both sides' status and time, and three differences: $.items[0].price, $.items[1].price and $.items[2].price, each a number in production and a string in staging.. Each captured request against staging, worst first. The detail pane lists the fields that differ.]*

## Run against both

1. Capture the requests you want to compare — a journey through the app, or your end-to-end tests — against one environment (say production).

2. Open **Tools › Compare Environments…**, or select requests, right-click and choose **Compare with Another Environment…**, or right-click a host in the sidebar and choose **Compare** it **with Another Environment…**

3. Set up the two sides:
   - **Left**: leave **Captured** on to compare with the responses you captured, or turn it off and give a scheme, host and port to send to a second environment too.
   - **Right**: the environment to compare with — its scheme, host (such as `api.staging.example.com`) and port. Empty fields keep the captured value.
   - **Headers…** on either side replaces headers for that environment, one per line as `Name: value`, such as its own `Authorization` token. A name with no value removes the header.

   Click a side's name to rename it (**Production**, **Staging**).

4. Choose the **Scope** — the selection, an app, a host, or all captured traffic — and click **Run** (<kbd>⌘</kbd> <kbd>↩</kbd>).

> **On Linux:**
> Right-click requests and choose **Compare Across Environments…**, or right-click an app or domain in the sidebar and choose **Compare _name_ Across Environments…**. In a Map Remote rule set to **Send a copy there and compare**, **Show Comparisons** opens the window on **Mirror Live Traffic**. **Run** is <kbd>Ctrl</kbd> <kbd>Enter</kbd>, and <kbd>Esc</kbd> cancels it.

Each request is sent straight to the servers, not through Cloak Proxy's tools and not into the traffic list. By default:

- one request per endpoint is sent (`/orders/42` and `/orders/57` are one endpoint);
- `POST`, `PUT`, `PATCH` and `DELETE` requests aren't sent, as they change data — they're listed as **Not sent**;
- static files, tunnels and CORS preflights are left out;
- four requests run at once, at most 10 per second per side.

> **Credentials and writes:**
> The captured request is sent with its headers, including its `Authorization` and cookies, unless you replace them under **Headers**. When those credentials would go to a different domain than they were captured for, or when writes are included, Cloak Proxy asks before sending. Only compare environments you're allowed to test.

## Read the results

The window's subtitle sums up the run: how many requests were sent and how many differ. Each row shows the request, both sides' status and time, how alike the two bodies are, and how many fields differ:

| Result | Means |
|---|---|
| **Status differs** | The two sides returned different status codes, such as `200` and `503` |
| **Error** | One side didn't answer: it refused the connection, timed out, or the TLS handshake failed |
| **Shape differs** | A field is only on one side, or has a different type (a number on one side, a string on the other) |
| **Values differ** | The same fields and types, with some different values |
| **Same** | The same status and body |
| **Not sent** | A write, left out unless you include writes |

Rows are sorted worst first; **Only differences** hides the ones that are the same. **Alike** is the body similarity score the [Authorization Matrix](https://docs.subcloak.com/cloak-proxy/testing/authorization-matrix/) uses, from 0 to 100%.

Select a row for its detail: both sides' status, time and host, and every field that differs — its JSONPath (`$.items[0].price`), whether it's only on one side, a different type or a different value, and the value on each side. **Compare** shows the two responses side by side, line by line. Right-click a row to show the captured request in the traffic list.

### What's ignored

- Fields that change on every call: timestamps, request and trace IDs, nonces, CSRF tokens, ETags (the same list the Authorization Matrix ignores).
- Key order and number formatting: `5` and `5.0` are the same.
- Anything under **Options › Ignore Fields…**: one JSONPath per line, such as `$.meta.build` or `$.items[*].stock`; a path covers the fields inside it.

When two arrays have different lengths, that's one difference on the array, and the items they share are compared one by one.

### Endpoint by endpoint

**Open in Release Diff** compares the two sides as two recordings in [Release Diff](https://docs.subcloak.com/cloak-proxy/testing/release-diff/): fields added, removed and retyped per endpoint, status changes and slower calls — useful when many requests share the same change.

## Mirror live traffic

Mirror mode copies live requests to a second host while you use the app, and compares each copy's answer with the original's. The app's own requests aren't changed or delayed: the copy is sent after the original finishes.

1. Open **Tools › Edit Map Remote Rules…**, add a rule matching your API (such as `https://api.example.com/*`) and set where copies go: the **Scheme**, **Host** and **Port** of the second environment.

2. Under **When a request matches**, choose **Send a copy there and compare**, and make sure Map Remote is on.

3. Use the app. In **Compare Environments**, choose **Mirror Live Traffic** to watch the comparisons arrive.

*[Screenshot: Mirror Live Traffic: the rule https://api.shopapp.example/* → https://api.staging.shopapp.example, 9 mirrored, 6 differ, 3 writes not mirrored, a Mirror writes too checkbox and an Edit in Map Remote button. The list compares Original and Mirror for each request, with GET /v2/users/me selected: Shape differs, 50% alike, $.loyalty only in Original and $.preferences only in Mirror.. Live requests copied to staging, each compared with the original's answer.]*

The bar above the list shows the mirror rules and counts: requests mirrored, how many differ, writes not mirrored and copies skipped while busy. **Mirror writes too** also copies `POST`, `PUT`, `PATCH` and `DELETE` requests — they change data on the mirror host. At most four copies are in flight and five are sent per second; requests that arrive while the mirror is busy are counted and skipped, not queued, so a busy app never floods the mirror. The latest 500 comparisons are kept, and **Clear** empties them with the session.

A mirror rule doesn't redirect anything, so a request the rule matches still goes to its own host and shows in the traffic list as usual.

## From an AI assistant

The [MCP server](https://docs.subcloak.com/cloak-proxy/automation/mcp-server/) has two tools:

- **`compare_environments`** sends captured requests (scoped with `ids`, `app` or `host`) to the `right` side — `{host, scheme, port, headers, name}` — and compares the answers with the captured responses, or with a `left` side given the same way. It returns the counts and each request that differs, with its fields. `include_writes`, `every_request`, `ignore_fields`, `concurrency` and `requests_per_second` work as in the window; `format` can be `markdown` or `json`.
- **`mirror_report`** returns the mirror rules and the latest mirror comparisons. Turn a mirror on with `upsert_rule` (tool `map_remote`, `"mirror": true`). On Linux, the app and the headless engine mirror too, so `mirror_report` works over `cloakctl` and MCP there.

```text title="Ask your assistant"
"Compare the ShopApp requests with api.staging.shopapp.example using Authorization: Bearer $STAGING_TOKEN,
 and tell me which endpoints return a different shape."
```

## In CI

`cloakctl compare-envs` compares a recording with another environment and exits with `2` when a difference matches `--fail-on`.

```bash title="Terminal"
cloakctl compare-envs --session prod-journey.har --right-host api.staging.example.com \
  --right-header "Authorization: Bearer $STAGING_TOKEN" --fail-on shape --out env-diff.md
```

| Option | What it does |
|---|---|
| `--session FILE` / `--har FILE` | The recording (required) |
| `--right-host HOST` | The environment to compare with (required), with `--right-scheme http\|https`, `--right-port N` and `--right-header "Name: value"` (repeatable) |
| `--left-host HOST` | Send to this environment too, instead of comparing with the captured responses (with `--left-scheme`, `--left-port`, `--left-header`) |
| `--host HOST` / `--app NAME` | Only this host's or app's requests |
| `--include-writes` / `--every-request` | Also send writes; send every request, not one per endpoint |
| `--ignore-field JSONPATH` | Leave a field out (repeatable) |
| `--insecure` | Accept self-signed certificates |
| `--format markdown\|json` / `--out FILE` | The output, and where to write it |
| `--fail-on RULE` | `status` (a status differs, or a side failed), `shape` (also fields and types), `values` or `any` (any difference), `never` (the default) |

| Exit code | Meaning |
|---|---|
| `0` | No request matched `--fail-on` |
| `1` | A usage error, such as a missing `--right-host` or an unreadable file |
| `2` | At least one request matched `--fail-on` |

## Limits

- Requests are replayed as captured. A request that depends on state on the server (an order that only exists in production) can differ for that reason alone; compare read-only endpoints, or use test data that exists in both.
- Values are compared exactly. Data that legitimately differs between environments (prices, stock, user records) shows as **Values differ**; ignore those fields under **Ignore Fields**.
- Only JSON bodies are compared field by field; other bodies are compared whole.
- Redirects aren't followed, so a redirect shows as its own status.
- Mirror mode copies requests only while Cloak Proxy is running and Map Remote is on, and only exchanges that completed.

## Related

  - [Map Remote](https://docs.subcloak.com/cloak-proxy/tools/map-remote/): Send requests to another server, or mirror them.
  - [Release diff](https://docs.subcloak.com/cloak-proxy/testing/release-diff/): Compare two recordings endpoint by endpoint.
  - [Authorization matrix](https://docs.subcloak.com/cloak-proxy/testing/authorization-matrix/): Replay every request as another user.
  - [Make tests from traffic](https://docs.subcloak.com/cloak-proxy/testing/make-tests/): Turn captured requests into a test suite.
