Behavioural drift¶
Every other check in this project asks whether reality matches the document. This one asks whether reality matches itself — whether the service is still doing what it was doing last month.
apiverity drift openapi.yaml \
--corpus april.har --against-corpus march.har \
--include-response-bodies
The cases it exists for¶
All three are contract-valid, which is exactly why nothing else catches them.
An optional field that stops being populated. email is optional, so a
response without it validates, drift --corpus reports nothing, and every
consumer that read it gets nothing. The shipped fixture pair
(fixtures/traffic/users-march.har and users-april.har) is this case, and a
test asserts that corpus drift finds zero findings on the later one — if that
ever stops being true, the fixture has lost its point.
A value that stops appearing. status still declares
[pending, active, archived] and archived has not been returned since the
migration. A consumer with a branch for it has dead code and no way to find
out.
A null rate that jumps. From 2% to 80%. Nullable is nullable, no schema check objects, and the meaning of the response changed anyway.
Why it mostly stays quiet¶
None of these is a defect on its own. A field that stops being populated may be a feature nobody uses; a missing value may just mean nobody hit that state this week. The value of the check is that it distinguishes a change from a coincidence, and one that reported both would be worse than none.
So:
- A minimum sample size, on both sides. Three responses and then two is not
evidence that a field was abandoned — it is two requests. The default floor
is twenty per operation (
--min-samplesto change it), and what could not be compared is counted in the report: "nothing changed" and "we could not tell" are different answers, and only one of them is reassuring. - A rate has to move a long way. Real traffic wobbles; a check that fires on noise is one people learn to ignore.
- Every finding carries its evidence — the observation counts on both sides — so a reader can judge the claim instead of trusting it.
- High-cardinality fields are not compared by value. Every id is new every time, and reporting that would be reporting that ids are ids.
What a profile is¶
Per operation, per response field: how many responses were seen, in how many
the field was present, in how many it was null, and which scalar values
appeared (up to a cap). Array positions collapse — items[0].status and
items[4].status are the same field — and one response counts once however
many array elements carried the field, or a list of fifty would make a single
response look like fifty observations.
That is small enough to commit next to a contract as a baseline, which is the point: keeping a profile is the alternative to keeping the corpus, and a corpus of real traffic is the thing you least want to keep.
Response bodies, and the safety model¶
The comparison reads response contents, so it requires
--include-response-bodies. That flag is opt-in for a reason and the reason
has not changed: a HAR of a real service holds real user data. Redaction runs
before anything reaches disk — see the safety model — and
the profile itself stores value sets under a cardinality cap rather than the
values of every response, which is why a profile is safe to commit where a
corpus is not.