Test your API
Release diff
Since 0.4.0 Requires Cloak Proxy 0.4.0 or later.
Record the same journey on two builds, or yesterday and today, and Cloak Proxy compares them endpoint by endpoint — endpoints added and no longer called, fields added, removed or retyped, status changes, slower calls, extra requests and new third parties receiving user data — with the requests on both sides, MCP and a CI exit code.
A release changes more on the network than its changelog says. A field becomes a string, a call fires three times instead of once, a new SDK starts sending the user’s email to an analytics company. Release Diff answers “what did this release change on the network?”: record the same journey on the old build and the new one, and it compares the two recordings endpoint by endpoint.
Schema changes spot a response that changes shape within one session. Release Diff compares whole sessions, so it also sees endpoints that stopped being called, requests that got slower or more frequent, and data that started going to someone new.
Every difference between two builds, most severe first. The detail pane shows the values on each side and the requests behind them.
- Choose what each side is
- Differences by kind
- A difference, with where it is
- The value before and after
- The requests on each side
Compare two builds
Section titled “Compare two builds”-
Record the journey on the old build: sign in, browse, add to the cart, check out — whatever the release touches.
-
Open Tools › Release Diff… (⇧ ⌥ ⌘ D) and choose Keep Current Traffic as Before. Cloak Proxy saves a copy of the session for the comparison, so you can clear the traffic list.
-
Clear the session, install the new build and record the same journey.
-
Choose After › Change › All Captured Traffic (or one app, host or version) and click Compare (⌘ ↩).
You don’t have to clear the session in between. When the app puts its version in its User-Agent (ShopApp/2.4 (iPhone; iOS 26.0), which most iOS and Android HTTP stacks do), or uses the in-app SDK, both builds can be recorded into one session: each side’s Change menu lists the App Versions it found. Right-click an app in the sidebar and choose Release Diff for the app to compare its two newest versions straight away.
Where each side comes from
Section titled “Where each side comes from”Each side’s Change menu offers:
| Source | What it compares |
|---|---|
| All Captured Traffic | Everything in the traffic list |
| App Versions | One app at one version, such as ShopApp 2.4 (52) (the number is its requests) |
| App / Host | One app’s or one host’s requests |
| Selected Requests | The requests selected in the traffic list. Right-click a selection and choose Diff Against Another Recording… to make it the after side |
| Session File… | A saved .cloaksession or .har file, such as last week’s recording or one from a teammate |
| Keep Current Traffic as Before | A copy of the traffic list as it is now (before side only) |
The cards show what each side holds once compared: its API requests, endpoints, and where it came from. ⇄ swaps the sides. When you change a side or an option after comparing, a bar offers Compare Again.
What’s compared
Section titled “What’s compared”Requests are grouped into endpoints the way Endpoints does — GET /v2/orders/42 and GET /v2/orders/57 are both GET /v2/orders/{id} — and each endpoint is compared across the two sides. Images, scripts, styles, fonts, tunnels and requests still in flight are left out.
| Difference | When it’s reported | Severity |
|---|---|---|
| New endpoint | Called after, never before | Low |
| Endpoint no longer called | Called before, never after. If the journey was the same, the screen that used it may have changed or broken | Medium |
| Field removed | A JSON field in successful responses before is in none after | High if it was in every response before; medium if it was optional |
| Field added | A field in responses after that wasn’t there before | Low |
| Type changed | A field was a number and is now a string, say (number → string) |
High in a response; medium in a request body |
| Now null | A field that was never null is null in some responses after |
Medium |
| Query parameter removed / added | The app stopped sending ?limit, or started sending ?sort |
Medium / low |
| Status changed | A status the endpoint didn’t return before: a 5xx or no response is high, a 4xx or a different success code is medium; an error that went away is low | High, medium or low |
| Content type changed | Successful responses were application/json and are now text/csv |
Medium |
| Slower | The median time is at least 1.5× the old one and at least 100 ms slower (both in Options) | Medium; high when over 3× and a second slower |
| More requests | An endpoint is called at least 25% more often and at least once more — a duplicated call, a retry or a new refresh | Medium; high when over 3× and three or more extra |
| More requests (whole journey) | The journey made at least 5 and 20% more API requests | Medium |
| New third party | A company receives user data after and received none before | High for personal data (email, phone, name, location, device, advertising and user IDs, your app’s tokens); medium for IDs from your backend; low when it’s contacted with no user data |
| Sends new data | A company already receiving data now gets a new kind, such as the email address | High or medium, as above |
| No longer sent data | A company stopped receiving user data | Low |
Fields are compared across every successful JSON response of the endpoint on each side (up to 60), request bodies across every JSON request body, and both are read after decoding gzip, deflate and Brotli. A field’s path is a JSONPath with [*] for array items ($.items[*].price); when a whole object is added or removed, only the object is listed, not each field inside it. Things that aren’t changes are left out: the same fields with different values or in a different order, an array that’s empty on one side, maps keyed by IDs ({"8812": …}), and cache-busting query parameters (_, t, ts, cb, nonce…).
Third parties come from the Privacy Map’s scanner run on each side, so the same data kinds and company names apply. The app’s own domains are guessed from its name and its busiest domain; set them in Options › App’s Own Domains… if the guess is wrong.
Read the results
Section titled “Read the results”The sidebar groups the differences: All differences, then Endpoints, Fields, Status codes, Speed, Request counts and Third parties, each with a count and its most severe icon. Hide low leaves out additions and the other changes that rarely break anything.
Each row says what changed (with the field path in the severity’s colour), one sentence about it, where it is — the endpoint, or the company and its domain — and, when there’s room, the value before and after. Select a row for its detail:
- the sentence in full, with how sure it is: “in all 3 samples before, in none of 2 after”, or “it was optional before, so this may be sampling”;
- the before and after values;
- the requests on each side that show the difference — click one to compare it with the first request on the other side, or Show in Traffic to select them in the traffic list (for a side from the captured traffic);
- the endpoint’s calls, median time and status codes on both sides, and its other changes.
Compare opens the two requests side by side, line by line, with JSON pretty-printed and keys sorted so formatting doesn’t count, and the changed field in bold.
Compare: the before and after requests line by line. Here the price became a string.
Every endpoint
Section titled “Every endpoint”Every endpoint lists each endpoint on either side, changed ones first: New and Gone badges, calls before → after, the median time (in red when it’s slower), the status codes on each side and how many differences it has. Select one to see its changes and its requests on both sides.
Every endpoint side by side: calls, median time, statuses and how many differences each has.
Every third party
Section titled “Every third party”Every third party lists each outside company either side talks to, with its requests before → after and the user data it receives: new kinds in red with a +, kinds it already received in grey, and kinds it no longer gets struck through.
Which companies receive what, before and after.
Options
Section titled “Options”| Option | Default | What it does |
|---|---|---|
| Slower when the median is | 1.5× the old one | How much slower an endpoint must be: 1.25×, 1.5×, 2× or 3× |
| And at least | 100 ms slower | Ignore small differences in fast calls: 25, 50, 100, 250 or 500 ms |
| Compare request bodies | On | Also report fields the app added to, removed from or retyped in what it sends |
| Compare third parties | On | Run the Privacy Map’s scanner on each side |
| Include images, scripts and fonts | Off | Compare static files as endpoints too |
| Ignore Fields… | None | JSONPaths to leave out, one per line, such as $.meta.generatedAt or $.items[*].views. A path covers the fields inside it, and $.items[3].x is read as $.items[*].x |
| Match Hosts… | None | When the sides use different hosts, pairs such as api.staging.shopapp.example = api.shopapp.example (the after host, then the before host), so their endpoints line up |
| App’s Own Domains… | Guessed | Registrable domains that belong to the app; everything else is a third party |
The sides and options are remembered. Kept traffic is stored on this Mac (only the newest copy), through a store that self-test and gallery runs redirect to a throwaway folder.
Export
Section titled “Export”The share menu exports the comparison as:
- Markdown — a table of both sides’ totals (requests, endpoints, hosts, errors, median time, data received), then a section per kind with each difference’s severity, endpoint, field, before and after values, and request numbers on both sides. It reads well as a pull request comment.
- JSON — the whole comparison as data: both sides’ totals, every endpoint, every difference and every third party.
From an AI assistant
Section titled “From an AI assistant”The MCP server’s release_diff tool compares two sides. Each side is an object: session_path for a saved .cloaksession or .har file, or nothing for the captured traffic, narrowed with app, app_version, host, ids, from_id and to_id. It returns both sides’ totals, the counts by severity and kind, each difference (severity, kind, endpoint, field path, before, after, a sentence and the request ids on both sides) and the third parties whose data changed — or the comparison as markdown or json. min_severity, category and limit narrow the list; the options above are arguments too.
"Compare ShopApp 2.3 with 2.4 in the captured traffic and tell me which changes could break the old app."See the MCP tools reference for every argument.
cloakctl diff compares two recordings and exits with 2 when a difference matches --fail-on, so a build can fail when a release changes the API in a way you care about.
cloakctl diff --before baseline.cloaksession --after e2e.har --fail-on high --out release-diff.md| Option | What it does |
|---|---|
--before FILE / --after FILE |
The older and newer recordings: .cloaksession or .har (both required) |
--session FILE with --before-version V --after-version V |
One recording with both builds in it, split by the app version in the User-Agent |
--app NAME / --host HOST |
Compare only this app’s or host’s requests on both sides |
--host-map NEW=OLD |
Line up hosts that differ between the recordings (repeatable) |
--first-party DOMAIN |
The app’s own domains (repeatable). Default: guessed |
--ignore-field JSONPATH |
Leave a field out (repeatable) |
--slower-ratio 1.5 / --slower-min-ms 100 |
When an endpoint counts as slower |
--include-static |
Also compare images, scripts, styles and fonts |
--no-third-parties |
Don’t compare third parties |
--format markdown|json |
The output format (default markdown) |
--out FILE |
Write the comparison to a file instead of printing it |
--fail-on RULE |
When to exit 2: a severity (high, medium, low — that or worse), kinds of difference (endpoints, fields, statuses, timing, requests, third-parties), single differences (field-removed, third-party-added…), comma-separated, or never (the default) |
| Exit code | Meaning |
|---|---|
0 |
No difference matched --fail-on |
1 |
A usage error, such as a missing or unreadable file, or a side with no requests |
2 |
At least one difference matched --fail-on |
A typical setup keeps a recording of the end-to-end tests from the main branch as the baseline, and compares each pull request’s recording with it. Record both with cloakctl run --record:
name: Release diffon: [pull_request]
jobs: diff: runs-on: [self-hosted, macOS] env: CLOAKCTL: /Applications/Cloak Proxy.app/Contents/Resources/cloakctl steps: - uses: actions/checkout@v4
- name: Record the end-to-end tests through Cloak Proxy run: | "$CLOAKCTL" run --ssl 'api.example.com' --record pr.har --quiet -- npm run test:e2e
- name: Compare with the main branch's recording run: | "$CLOAKCTL" diff --before baseline/main.har --after pr.har \ --fail-on high,third-parties --out release-diff.md
- name: Keep the comparison if: always() uses: actions/upload-artifact@v4 with: name: release-diff path: release-diff.mdLimits
Section titled “Limits”- Release Diff compares what was recorded. Request counts and the “no longer called” check assume both sides cover the same steps; a shorter journey reads as missing endpoints.
- Fields come from samples. An optional field that happened not to appear on one side can read as removed or added; the detail says how many samples had it, and optional fields are never reported as high.
- Timing depends on the network, the device and the server’s load. Record both sides in the same conditions, and repeat the journey when a difference is small.
- Only JSON bodies are compared field by field. Bodies cut short when captured, and bodies over 4 MB, aren’t sampled.
- Endpoints line up by method, host and path template. An endpoint that moved to a new path reads as one removed and one added.
- Request numbers are each recording’s own. Sessions saved by Cloak Proxy 0.3 or earlier store times to the whole second, so their durations are approximate; save them again to keep milliseconds.