App radarbrowse guides

The app-change radar

A scheduled structural crawl of your own app that reports what changed and which tests it puts at risk — deterministic, and it never clicks anything.

A test suite tells you when something broke. It tells you nothing at all about the thing that broke it, and it tells you that only after somebody ran it.

The radar looks from the other end. On a cadence you set, it visits your pages, fingerprints their structure, and compares each one against the last structure you accepted. What comes out is a short feed of differences in the app, each one carrying the list of tests that difference puts at risk — before anyone has run them.

The radar is a Pro plan feature, and it is deterministic end to end. No model is called, no AI key is spent, and an org with no key gets the whole radar and the whole at-risk list.

What a sweep does

A sweep takes one browser, one context, and opens one page per URL. It performs exactly two operations on each: navigate, and read the DOM.

It never clicks. Not a consent banner, not a cookie wall, not anything. That is a deliberate cost rather than an oversight: the crawler runs unattended, on a timer, carrying a real user's captured session, and the words on a consent button ("Accept", "Allow all", "I understand") are also the words on "Delete this workspace?". So on a site with a consent wall the radar fingerprints the consent wall — a wrong answer, chosen over a dangerous one. Put the radar behind a role whose session has already dismissed it.

Every URL is fenced to the base URL's origin. A seed pointing elsewhere is rejected and reported; a link discovered mid-crawl that leaves the origin is not followed. URLs are normalised before comparison — the fragment and the query string are dropped, so /orders?page=2 and /orders?sort=asc are one page. A screen whose identity lives in its query string is invisible to the radar.

What a fingerprint is, and what it deliberately ignores

The radar's promise is no news is no message. A feed that lights up because somebody reworded a button is not a signal, it is a reason to switch the feature off. So a page's fingerprint is built from element identity only:

HashedIgnored
tag, type, role, accessible name, data-testid, idvisible text, label, placeholder, aria-label, CSS path, page title, element order

An element with no copy-free anchor is dropped from the fingerprint rather than hashed on a wobbly key — a button identified only by the word on it cannot be matched across two snapshots without matching on that word, which is the one thing this refuses to do. Drops are counted and reported in the sweep summary, never silent.

The practical consequence: rewording your copy produces no radar rows. Adding, removing or re-identifying a control produces one.

The feed

Open it at Quality → App radar on a project. Each row is one open question about one URL, of one of three kinds:

KindMeaning
New pageA URL that was not in the baseline.
Page goneA baselined URL that the sweep could not fingerprint.
Page changedElements added, removed, or re-identified.

There is at most one open row per page. A sweep that still sees the same difference updates that row's "last seen" rather than stacking a duplicate, so the feed counts open questions, not sweeps. Suspect rows sort to the top; the rest is newest-first. Below the feed, Watched pages lists every URL the radar has captured, with its capture count and whether it has an accepted baseline yet.

The first time a URL is fingerprinted it becomes its own baseline, so a brand-new page does not open a change. That is why the very first sweep of a project is almost always quiet: it is building the thing later sweeps compare against.

A page the crawler could not load is not reported as gone. Only an observable HTTP error status produces "Page gone" — a flaky navigation must never read as a deleted page.

Under each row is the at-risk list: the tests that either click an element the change touched (uses a moved element) or navigate to that page (visits this page), selector matches ranked first. A change with no tests under it is labelled plainly — no test clicks an element here or navigates to this page, this change is unguarded — which is usually the more interesting row of the two.

The at-risk list asks the run-selection gate, so a machine-written test still waiting for review is never named as coverage you have.

Clearing a row

Three buttons, and the difference between them is what happens to the baseline:

  • Accept as expected — the baseline advances onto the snapshot just taken. The row closes and the same difference is never reported again. This is the button for "yes, we shipped that".
  • Mark suspect — the row stays open and the baseline does not move. It keeps reappearing until the app goes back or somebody accepts it. Suspect is deliberately not an exit: it means "this looks like a regression", and quietly forgiving it would be the same lie as never reporting it.
  • Dismiss — closes the row without moving the baseline, for a difference not worth tracking.

A row also closes on its own: if the app returns to its baseline shape, the next sweep resolves it as reverted.

Configuring it

Configure on the radar page opens the settings:

SettingBoundsDefault
Enabledon/offoff
Cadencehourly, daily, weekly, manual only
Seed URLsup to 200, one per line, all on the project's originthe project base URL
Max pages1–20025
Max depth0–52

Four more settings exist on PUT /api/projects/:projectId/radar with no field in the dialog: elements per page (10–400, default 120), the sweep deadline (10 s–30 min, default 5 minutes), and the environment and auth role the sweep runs as. Pinning an environment and a role is what makes the radar sweep staging as a logged-in user rather than the marketing page as an anonymous one — worth doing, and for now an API call.

Manual only never schedules a sweep; Run sweep on the page starts one immediately whatever the cadence says.

Every bound reports whether it bit. A sweep that hit the page budget or the deadline lands as a partial sweep and says which bound stopped it, because a truncated crawl reported as a clean one is the failure this feature could most easily hide.

Reading an empty feed correctly

The empty states say three different things and they are not interchangeable:

  • No baseline yet — the radar has never swept. An empty list means nobody has looked, not that the app is unchanged.
  • Nothing changed since your last review — the last sweep found no structural difference the baseline does not already account for. This one is the goal.
  • The radar could not be loaded — a fetch failed. This is not a clean sweep.

Two sweeps never overlap

A scheduled sweep is claimed in the database, so several replicas cannot both run it. A manual sweep on a project already sweeping is refused with a clear message rather than started. Both are correctness guards: two concurrent sweeps would read the same baselines and write two rows for the same URL, which is exactly the duplicate the feed is shaped to prevent.

The scheduler runs at most three sweeps per tick across the whole deployment and measures the next due time from now, not from the missed one — a server that was off for three days owes a daily radar one sweep, not three. A scheduled sweep that threw is written up as a failed sweep in the normal shape, so the status bar never shows last week's success.

If you have notifications on, the daily digest reports unreviewed radar changes across the workspace. It is silent when there is nothing unreviewed and silent again when nothing has changed since the last one.

From an agent

An MCP client can read the feed with get_app_changes — the open changes for a project (optionally only those since a timestamp), each with its at-risk tests. It leads with a state of not-configured, never-swept, clean or changes-open, so an agent can tell "nothing is wrong" from "nobody has looked".

That is the whole radar surface over MCP. Sweeping, configuring, and accepting or dismissing a change are dashboard and API actions — none of them has a tool.

Limits worth knowing

  • Pro plan and above, including the read side. The feed is the product, not a preview of it.
  • The crawl is structural. It sees controls appearing and disappearing; it does not see a wrong total, a broken calculation or a changed policy. That is what business rules and actual tests are for.
  • A page behind a consent wall, a login the configured role cannot pass, or a client-side route the crawler cannot reach by link is fingerprinted as whatever it could load — which is why a partial sweep is labelled as one.
  • A control identified only by an unstable id (a generated :r0:, a uuid, a long digit run) is dropped from the fingerprint rather than reported as churning. The drop is counted, not hidden.
  • A control the radar could only anchor by being the only one of its tag on the page leaves the fingerprint the moment a second one appears — reported as a removal, which is a real structural change described in unhelpful words.
  • The radar's own history is not swept on a retention schedule today. Snapshots and resolved changes accumulate.
  • Changing how fingerprints are computed invalidates every baseline; they are version-stamped so that is visible rather than silent.

See also