Catch a silent selector rename in scraped HTML
Scrape an HTML page into a structured object with transform, then drift the structured shape against its schema so a markup or selector rename becomes a hard contract error instead of a silently missing field.
Task
You scrape a page that has no API — a catalog provider returns HTML, and you
parse it into a structured object with a selector. The danger is silent: the
provider renames a CSS class or moves a cell, your selector quietly stops
matching, and the field comes back undefined. Nothing throws; downstream code
keeps running on corrupted data until someone notices a ranking is wrong. You
want that markup rename to surface the moment it happens, as a loud error on the
structured shape — not the raw HTML.
Example
The response pipeline is transform → pick → validation, so transform
reshapes the HTML into a structured object before drift ever runs — drift
then validates the parsed value, not the raw markup. Declare score as
required in the schema: its loss is the breakage you want caught.
import { , } from 'stitchapi';
import { } from 'zod';
// A trivial hand-rolled scraper: the `score` selector keys off `td.score` —
// exactly the class a markup rename silently breaks.
function (: unknown): { : <string, unknown>[] } {
const = ();
const = .(/<tr[^>]*class="row1"[^>]*>/i).(1);
const = .(() => {
const : <string, unknown> = {};
const = /<td[^>]*class="title"[^>]*>([^<]+)<\/td>/i.();
const = /<td[^>]*class="score"[^>]*>\s*(\d+)\s*<\/td>/i.(
,
)?.[1];
if () ['title'] = [1]!.();
if ( !== ) ['score'] = (); // omitted when the selector misses
return ;
});
return { };
}
const = ({
: 'https://api.example.com',
: '/catalog',
: , // HTML string -> { items: [...] }
: 'items',
: (
// `score` is REQUIRED — its loss is the breakage we want caught.
// A required field that goes missing throws (`STITCH_DRIFT`, `change: 'invalid'`).
.(.({ : .(), : .() })),
),
});
const = await ();How it works
Each call parses the page and validates the transformed shape —
[{ title, score }], not the HTML body — against the schema. No baseline to
generate or commit: the schema is the contract.
When the provider renames the score cell's class (score → rank), the
selector stops matching and scrape drops score from each item. Because the
schema requires score, validation sees a missing required field and throws
STITCH_DRIFT — a finding with
{ level: 'error', change: 'invalid', path: '[].score' }. The path is the
proof: [].score exists only on the parsed object, so a finding on it can only
come from drift inspecting the transform output, not the raw markup. The
silent undefined is now a loud, immediate failure.
Other non-fatal soft-drift findings (undeclared, coerced, defaulted) ride
the event stream without throwing — you can
watch them before any becomes a
hard failure.
Declaring score required is the whole trick. Drift honors the schema's
optionality: if score were .optional(), a response without it would
validate clean and the rename would slip past — which is correct for a field
that genuinely may be absent. Make a field required precisely when its
disappearance is a bug.
Anti-pattern: do not drift the raw HTML body and skip transform.
Snapshotting the markup makes every cosmetic edit — a reordered attribute, a
whitespace change — look like drift, burying the one rename that matters.
Parse to the structured shape first, then rely on required fields in the
schema to catch the breakage you actually depend on.
See also
Catch a breaking API change before your users do
Wrap output with drift to validate every response against the declared schema and catch a dropped required field, a type coercion, or an undeclared new key.
Inspect what the server actually sent
Probe a fresh call with .inspect() to read the unredacted raw body next to the validated value and the drift findings diffed between them — without throwing — when you need to see what changed after the fact.