# HarpyWatch — The Nest, in full
Every published post, concatenated. Canonical addresses are under https://harpywatch.com/blog/. Each post is also available on its own at the URL given beneath its title.
---
## Staging configuration travels, and agents help it
URL: https://harpywatch.com/blog/staging-configuration-travels
Author: The HarpyWatch team
Published: 2026-10-01
Kind: article
Tags: AI Agents, Deployment, SEO
The fastest way to get something working is to copy the configuration from
wherever it already works. It is a reasonable technique and most of us have
used it, and an agent asked to make a deployment work tends to reach for it for
the same reason we do: it is the shortest path from broken to working, and
nothing in the task says otherwise.
Configuration copied from staging is configuration that says staging — in a
hostname inside a , in an X-Robots-Tag header, in the
origin an API is willing to answer. None of those is contradicted by anything
running on your machine.
What actually travels
These are not hypothetical. They are the ordinary residue of an environment
that was cloned rather than derived.
A canonical tag pointing at the staging hostname. The template renders
on the
production page. Every browser renders the page normally. Search engines are
told, by you, in your own markup, that the authoritative copy of this page
lives at an address they cannot reach or should not index. The page is live,
correct and quietly asking to be excluded.
A noindex that shipped. Staging carries X-Robots-Tag: noindex or a meta
equivalent so it does not compete with the real site. Correct. Then it rides
along in the image, the config map, or the nginx snippet somebody copied, and
the production site is now instructing crawlers to drop it. The page renders
perfectly. Traffic decays over weeks and the cause is a header nobody reads.
A test API key. Payments succeed in the test ledger and never charge
anybody. Email sends to a sandbox that accepts everything and delivers nothing.
The application logs success on every path, because from its point of view
every path succeeded. This one is particularly cruel: it fails by working.
A database URL. Production pointed at the staging database, or worse,
staging pointed at production and now writing to it. The first is a site whose
data quietly does not persist where anyone expects. The second is a test suite
mutating real rows.
A CORS origin. Access-Control-Allow-Origin: https://staging.example.com,
served in production. The API answers every request from a browser at the real
hostname with a response the browser then refuses to hand to the JavaScript
that asked for it. The server's logs show 200s. The user sees an empty panel.
The developer, testing from a tool that does not enforce the same-origin
policy, gets a clean 200 and moves on.
A robots policy. Not just noindex — a robots.txt with Disallow: /, or
a sitemap reference pointing at the staging host, or a crawl-delay written to
stop a load test from hammering a small box.
Why none of these show up locally
Every item on that list is a fact about the response a stranger receives from
a particular deployed hostname, not a fact about the code. The repository is
identical in both environments. The difference is in what an environment
variable, a config map or a build-time substitution put there, and by
definition that substitution does not happen on the machine where the code was
written.
So the local check cannot see it. The unit test cannot see it — it does not
make an HTTP request to a public hostname. And whoever verified the change,
human or agent, verified it against the dev server, where the canonical tag
rendered a localhost URL and nobody minded.
And the browser will not tell you either. A canonical tag, a robots header and
a CORS origin are all invisible in rendered output. You have to look at the
headers and the markup as delivered, at that address, from outside.
Check the deployment, not the intention
The correction is to stop treating "it passed on my machine, and the deploy
reported success" as evidence about production, and start observing production
as its own object. Observation does not prevent any of this — the wrong header
still ships — it changes how long the wrong header is live before somebody
knows. For a canonical tag or a robots policy, that interval is the entire
cost.
Three checks cover most of what travels. They do not cover all of it — the
limit is at the end of this section, and it is a real one:
- http-headers reads the headers the deployed host actually returns —
including X-Robots-Tag, cache directives and anything else that arrived in
a copied config file. You assert what should be there; the check tells you
what is.
- robots-policy fetches robots.txt and the meta and header equivalents,
and tells you whether this hostname is currently asking to be indexed or
asking to be forgotten. Two environments should disagree about this, and only
one of them should say Disallow: /.
- cors-policy makes the preflight request from an origin you name and
reports what came back. This is the only way to see the failure, because the
server does not consider the request an error and the browser's refusal
happens after the response has already been logged as a 200.
None of these three detects a test API key. That is a real limit and worth
stating: from outside, a payment that succeeds in the test ledger looks exactly
like a payment that succeeded. Credential provenance is something only your own
deployment process can assert, and the fix is that production secrets come from
a store nothing else can read, not from a file that was copied.
Why an agent makes this worse, specifically
The difference between a person doing this and an agent doing it is not care.
It is that a person who configured both environments by hand ends up knowing
how the two differ, and that knowledge is what makes them stop over a pasted
value whose origin they cannot name. The pause is a side effect of having done
the work slowly, and it is doing more safety work than anyone credits it for.
An agent produces a configuration that is quick, plausible and mostly correct,
and nobody ends up holding that map. Not the agent, whose context for the task
ends with the task. And not the reviewer, who is reading a diff — where the
value appears as a variable name — rather than the substitution that fills it
at deploy time. There is nothing to be uneasy about, because nobody remembers
making a decision.
Which is why the check has to be external and automatic. The environments no
longer differ in ways anyone is tracking. They differ in ways only the deployed
response can tell you about.
The cheapest moment is before DNS
There is a version of this that costs almost nothing. Run the checks against
the staging URL — the real deployed staging host, not localhost — before you
point production at it, or before you promote the image.
At that moment every value on the list above is observable at an address that
exists, and none of them is affecting anybody
yet. You are not looking for staging to be perfect; you are looking for the
list of things that are true of staging and must not become true of production.
That list is short and reading it takes a minute. The alternative is finding
out from a search engine, and a page dropped from an index does not come back
the moment you fix the header — it comes back on the crawler's schedule, which
for a small site is measured in weeks.
---
## Monitor the site nobody is looking at yet
URL: https://harpywatch.com/blog/monitoring-before-you-have-users
Author: The HarpyWatch team
Published: 2026-09-29
Kind: article
Tags: AI Agents, Monitoring, Reliability
A site with ten thousand users has a monitoring system whether or not anyone
built one. It is called the support inbox. Something breaks, and within minutes
there are three tickets, a tweet and somebody's colleague saying it works for
them. The signal is noisy and slow, but it exists.
A site with no users has none of that. It has visitors — a few a day, from a
link somewhere — and most of the ones who hit a broken page close the tab.
They do not file a bug. They were never going to. The outage is silent and it
costs you exactly the people you were trying to get.
Which inverts the usual instinct. Monitoring feels like something you add once
a site matters enough to protect, but user reports are the only detection an
early site has, and it does not have any users. What is left is you, whatever
you last deployed, and however long it takes you to look again.
The specific way a new site breaks
It is rarely the server falling over. A new site is usually one process on one
managed host serving very little traffic, and that arrangement is hard to
overload. The failures are of a different kind, and most of them share a
property: the site returns 200 the whole time.
The domain resolves to nothing, because the nameservers were changed and the
old record is still cached in your browser and nowhere else. The certificate
was issued for www. and the apex was never added, so half the people who type
the name in get an interstitial. The redirect from the old landing page loops.
A robots.txt written for the staging build is still telling every crawler to
go away. An API route the front page calls returns a 500 for anyone not
carrying the session you had open when you tested it.
Every one of these works on the machine it was built on. Several are invisible
even in a browser, if it is the browser that has been open since before the
change. And in a workflow where the config, the redirects and the deployment
were written by a coding agent in a single session, nobody chose any of them
deliberately — the build passed, the session ended, and the gap between that
and a working public site is whatever nobody read.
What to watch on day one
The useful first setup is small enough to be embarrassing. That is the point of
this piece: it is not a project, it is a few minutes.
| Check | The failure it catches |
| --- | --- |
| http-uptime | The site is not serving. The baseline; everything else assumes it. |
| dns-resolution | The name does not resolve, which looks identical to the site being down but is fixed somewhere else entirely. |
| ssl-cert-expiry | The certificate on the wire is expired or about to be. Total outage, entirely predictable. |
| redirect-chain | Apex to www, HTTP to HTTPS, and old paths to new ones — the hops a visitor actually makes. |
| dom-content | The page returned 200 and rendered the wrong thing. A framework error page is a 200 in a lot of setups. |
| robots-policy | You are still telling search engines not to index you. |
That is six checks against one URL. They have to run from outside your own
infrastructure, for a reason that only bites during a total failure: a check
running on the same host, in the same container or behind the same network
segment reports nothing once that host, container or segment is the thing that
went down — precisely when you needed a report.
One addition is worth making immediately after: point the same set at your
staging URL before you switch DNS to it. Finding a broken redirect or a
noindex that travelled from staging is cheap before the traffic arrives and
awkward afterwards, and it is the same six checks against a different hostname.
What can wait
Being honest about the other side of this matters more than adding items.
Performance budgets can wait. core-web-vitals, ttfb-budget and
page-weight measure things worth measuring, but the number they give you on a
site with no traffic is a number with no consequence attached. Set them when
you have a baseline to regress from.
Most security headers are not urgent yet. csp-policy in particular is
worth doing properly, and doing it properly means understanding what your pages
load. A content security policy added on day one to satisfy a checklist tends
to be either so permissive it does nothing or so strict it breaks the site the
first time you add an embed.
Anything whose value scales with page count is premature. broken-links and
sitemap-health are genuinely useful and they get more useful as the site
grows. On a five-page site you can read it yourself in less time than
configuring the check.
Alerting on everything is a habit not worth acquiring. With six checks and one
site, alerts on all of them are fine because the volume is nothing. The
discipline you will need later is deciding which of them are worth waking you
up. Start by paging on http-uptime and ssl-cert-expiry and reading the rest
when you look.
Why this is smaller than it sounds
Monitoring has a reputation for being a platform: agents to install, dashboards
to build, a retention policy to argue about. That reputation is earned, and it
is earned by instrumenting your own infrastructure, which is a genuinely large
problem and not the one on the table here.
Watching a website from outside has no install step, because the check connects
the way a visitor does, and no instrumentation step, because the observable is
the response. What you supply is short enough to name exactly: the URL, an
interval, credentials for anything behind a login, and one address that
receives the alerts. That last item is the one
people skip, and a check with nowhere to report is a check that has changed
nothing.
The corresponding limit is worth naming too. An external check sees what a
visitor sees, which means it cannot see anything a visitor does not: a job
queue that has stopped draining, rows written to the wrong database, an
inconsistency that only appears after a login you did not give it. Those need
instrumentation from inside. What outside observation is for is the class of
failure where the site itself is unreachable, wrong, or lying to crawlers —
which on a new site is most of them.
This is what HarpyWatch does — checks run from outside your infrastructure on a
schedule, results in one grid, alerts by email, Slack or webhook. Configuring
the list above is a form, six times. Deciding what should alert you and
verifying that the alert actually arrives takes longer than the configuring
does, and it is the part worth doing carefully.
What that buys is a shorter time to discovery. Without monitoring, the gap
between a site breaking and you knowing is however long until you next look —
on a site you are not yet promoting, that can be weeks. With it, the gap is the
check interval, plus delivery, plus however long the alert sits unread. None of
those three is zero. All three together are typically minutes, against weeks.
Everything else here is detail; that substitution is the argument.
---
## The API contract changed underneath the frontend and nothing failed
URL: https://harpywatch.com/blog/the-contract-changed-underneath
Author: The HarpyWatch team
Published: 2026-09-24
Kind: article
Tags: AI Agents, Monitoring, Reliability
uptimepercent used to be a number. It is now a string, because a Rust
serialiser gained a format!("{:.1}", pct) to round it to one decimal place,
and format! produces text.
The backend tests pass: they assert on the value, and "99.9" is the value. The
frontend builds: TypeScript is describing a response it was told about, not one
it received. The page renders. In the cell where a percentage belongs there
is now 99.9%%, or NaN%, or nothing at all, depending on which arithmetic ran
first.
Nothing in either build raised a hand.
Why the build cannot save you here
The failure has a precise location: the boundary where a typed program stops
being able to check its own beliefs.
Inside the backend, types are enforced. Inside the frontend, types are enforced.
Between them is JSON, which enforces nothing. The frontend's
interface UptimeSummary is not a check on anything — it is a comment the type
checker happens to trust. When the two sides disagree, the disagreement lives in
the one place neither language is looking.
Every failure in this family is the same shape:
- A field changed type: number to string, string to enum object, scalar to
array of one.
- A field was renamed: openIncidents became openincidents, so the old
name is now undefined and undefined renders as an empty cell.
- A field became optional and is now absent on some rows — usually the
interesting rows, the ones where a check has not run.
- A field got nested: { latency: 42 } became { latency: { p50: 42, p95: 81 } }, and the template now stringifies an object.
- An array became paginated: what was [...] is now { items: [...], next: null }, and .map is not a function.
None of these throw on the server. Most do not throw on the client either.
JavaScript's defining characteristic in this situation is that it keeps going.
An absent value propagates through template interpolation and arrives on screen
as blank, as the literal word undefined, or as NaN once it has been through
a calculation. The page looks like a page. It is simply wrong.
What agents changed about the odds
This has always happened, in every codebase with a client and a server written
by different people. What changes when the writing is done by agents is that the
two sides of a contract can now be edited hours apart by processes that share no
memory, and the diff each produces is small enough to approve without opening
the other side.
When the API and the client are edited from two separate sessions, neither
session holds both sides in context. Each makes a locally correct change. One
was asked to round a percentage and did so cleanly, with a test. The other was
asked to add a sparkline and did so cleanly, with a test. Nothing anywhere
contained both facts at once.
It happens with one agent across two sittings for the same reason: the context
that knew the response shape ended when the session did.
And the pull request looks good. A diff adding format!("{:.1}", pct) is small,
tidy, well-named and obviously improving something. It gets approved quickly,
because the question a reviewer asks is "is this change correct" and the answer
is yes. The question that would have caught it — "who else believes this field
is a number" — needs the whole set of consumers in view.
There is machinery for that. A checked-in OpenAPI or GraphQL schema with
generated clients moves the disagreement to build time, and if you have it,
use it — it is strictly better than what follows. The gap is that it only covers the endpoints
described by the schema, and it is a statement about the code in the repository
rather than the pair of services currently deployed.
Integration tests test the version you had
The usual answer is contract testing, and it is a good answer that has a
specific hole.
An integration test asserts that the API and the client agree at the moment
CI ran, against the code in that branch. That is genuinely valuable. It is
also a claim about a repository, not about a deployment.
The things it does not cover are exactly the things that bite:
- The API is deployed and the frontend is not, or the reverse. The two versions
agreed in the repository and do not agree in production.
- The response shape depends on data. The test fixture has a monitor with
results; production has one that has never run, and that row is the one with
the null.
- Something upstream changed. A third-party API you proxy altered its own
payload, and your serialiser passed the change straight through.
- A cache, a proxy, or a CDN is serving a body from before the deploy.
- A rollback restored an older API against a newer client.
In every case CI was green and the deployed pair disagree. The assertion has to
run against the running system, on a schedule, or it is answering a question
about the past.
Assert the shape, continuously
The correction is unglamorous, and it is detection rather than prevention: check
that the deployed endpoint still returns what the deployed client expects,
repeatedly, from outside. It will not stop the deploy. It will tell you within
one check interval instead of within one customer email.
HarpyWatch does this with two checks that do different amounts of work.
json-api requests an endpoint and asserts on the response body itself —
that a path exists, that a value matches, that a field is present. It is the
right tool when there are two or three facts about a response you actually care
about and you do not want to describe the rest of it.
json-schema validates the whole body against a JSON Schema you supply.
This is the one that catches the type change, because a schema says
"type": "number" and "99.9" is not one. It also catches a field going
missing when the schema marks it required, and a field going nested when the
schema says it is a scalar. The check fails at the endpoint that changed rather
than three services downstream, which is what makes the failure legible instead
of a hunt.
You do not need a schema for every endpoint. You need one for the handful whose
breakage would render a screen wrong without erroring — the summary payloads,
the counts, the anything-a-dashboard-draws. For most products that is a handful of
endpoints, not the whole API surface.
Two honest limits
First, a schema is a thing you wrote, and it can rot. If the API legitimately
gains a field and nobody updates the schema, you get either a false failure or,
if the schema is permissive, a silent gap. Schemas need to live next to the code
that produces the response and change with it.
Second, this catches shape, not meaning. uptimepercent: 0 is a perfectly
valid number and may be catastrophically wrong. No schema will tell you that a
value is stale or fabricated — only that it is the right type. Shape
validation is the floor, not the ceiling.
Both limits are worth accepting, because the failure being prevented is the one
that produces a confident, populated, entirely wrong screen — and nothing in either
codebase's type system is going to notice it.
---
## Four hops to nowhere: what refactors do to your redirects
URL: https://harpywatch.com/blog/four-hops-to-nowhere
Author: The HarpyWatch team
Published: 2026-09-22
Kind: article
Tags: Deployment, Monitoring, SEO
A link in a two-year-old newsletter is clicked, and here is what happens before
the reader sees anything:
http://example.com/blog/post-name
→ https://example.com/blog/post-name (301, http to https)
→ https://www.example.com/blog/post-name (301, canonical host)
→ https://www.example.com/articles/post-name (301, section rename)
→ https://www.example.com/articles/post-name/ (301, trailing slash)
→ 404
Five requests. Five DNS-and-connection round trips in the worst case. And the
destination does not exist, because the article was retired in a content cull
and the redirect rule was written against a pattern rather than a list.
Four redirects, added eighteen months apart, each correct when it was written,
and none ever deleted — because deleting a redirect is how you break the thing
it was protecting. Chains are an emergent property: whoever added the fourth
was not looking at the first three, and no file anywhere shows all four
together.
A coding agent changes the rate. Asked to rename a route, it will add the
redirect, which is more than most humans manage — forgetting is the older
failure. But it adds it at whichever layer it happens to be editing: the
framework's route table, or nginx, or the CDN config. Three layers, no single
file that shows the whole picture, and nothing in the task that prompts it to
check whether the target of its new redirect is itself a redirect.
The four shapes, and what each one does
The chain. Two or more hops to reach content. Every hop is a full request
and, when it crosses an origin, a fresh DNS lookup, TCP connection and TLS
handshake. That is several round trips before any bytes of content, so the cost
scales with latency rather than bandwidth: on a mobile connection with 100ms
round-trip time, a cross-origin hop costs several such
round trips on its own, and a faster pipe does not help.
Google's documentation says Googlebot follows up to ten redirect hops in a
single crawl attempt and advises keeping chains short; treating three or more
as a defect is the conventional reading and it is sound. How much link signal survives a
long chain is disputed, and hard for you to measure either way — which is
itself the argument. Point the link at the final destination and the question
never arises.
The loop. /a redirects to /b, /b redirects to /a. Browsers stop
after a fixed limit — Chrome at twenty hops — and show
ERRTOOMANYREDIRECTS. A crawler gives up and drops the URL. This is a total outage for that page and it is
invisible to anyone whose browser has cached an earlier answer, which
frequently includes the person who deployed it. The classic cause is two rules
in two layers disagreeing: nginx forcing a trailing slash while the application
strips it.
The mixed http/https hop. A chain that dips into http, even for one hop,
sends that request in the clear. Any header on it — a cookie without Secure,
a session token in a query parameter — is on the wire. It also gives anyone on the path — a
compromised router, a hostile access point — the chance to rewrite that
response and send the browser somewhere else entirely, before HSTS has had a
chance to apply.
The usual shape is http://old.example → http://new.example →
https://new.example: someone fixed the host and left the scheme to the layer
below, which is one hop too late.
The redirect to a 404. The most common one, and the most damaging. A
pattern-based rule (/blog/(.) → /articles/$1) is written when the sections
matched. Then articles get retired, and the rule cheerfully maps a live inbound
link to a page that does not exist. A 301 into a 404 is worse for you than a plain 404 would have been: the
original URL has been declared moved, and the destination it was moved to
returns nothing. You have retired the old page without producing a new one.
Here is how the treatment differs, roughly:
| Shape | What a browser does | What a search engine does |
| --- | --- | --- |
| One hop, 301 | Follows, caches the redirect | Passes signals, indexes the target, eventually replaces the old URL |
| Chain of 3+ | Follows, pays the latency | Follows, but crawl budget is spent and Google's own advice is to shorten it |
| Loop | ERRTOOMANYREDIRECTS | Drops the URL |
| Any http hop | Follows, request sent in the clear | Follows; the insecure hop is a security finding, not an SEO one |
| 301 into 404 | Shows the 404 | Deindexes the old URL, finds nothing at the new one |
One more distinction, routinely got wrong: a 302 says "keep the old URL,
this is temporary". Six months of a 302 that was always meant to be permanent
means the old URL is still the indexed one. Generated code uses whatever the
framework's redirect() helper defaults to, which is often 302. Check yours.
Why you cannot see this from inside
You can read your nginx config, your route table and your CDN rules and still
not know what happens to a request, because the answer is the composition of
all three plus whatever the CDN does with trailing slashes on its own
initiative. You can reconstruct it from edge logs afterwards, if you have them
and the patience. Making the request from outside and following it answers the
question directly.
That is what HarpyWatch's redirect-chain check does — it is our product, so
weigh the recommendation accordingly. It starts at a URL, follows every hop,
records each status code and location, and reports the full path, the number of
hops, whether any hop was insecure, and whether the terminus is a loop or an
error. Its companion broken-links catches the last shape in the table: it
crawls what your pages link to and reports what does not resolve, including the
links that resolve through three hops into a 404 that clicking around the site
would never reach. Neither is a clever check. Both answer a question that is easy to assume you
already know the answer to.
The limit is real: an external check follows the links it can discover from a
public page, plus whatever URLs you give it explicitly. It does not know about
the inbound link in somebody else's PDF or the QR code on a printed flyer —
exactly the class of link redirects exist to protect, and the class most likely
to point at a URL no one has considered since 2021. For those you have only the
redirect configuration itself, which is an argument for keeping it in one
readable place rather than in three.
Do it before DNS moves
A restructure is a scheduled event. The URLs that will move are known before
the deploy, and so are the chains they will create: both are properties of
configuration that already exists in a branch.
So put the new site on a staging hostname and run the checks against that
before you point anything at it. Take your most-linked URLs, run them through,
and look at the hop counts. A chain found at that moment costs one line in a
config file. The same chain found a month later costs whatever the traffic to
those URLs was worth — and you will not be able to measure that, because
traffic that did not arrive leaves no record anywhere.
---
## Eleven domains nobody approved
URL: https://harpywatch.com/blog/eleven-domains-nobody-approved
Author: The HarpyWatch team
Published: 2026-09-17
Kind: article
Tags: AI Agents, Monitoring, Reliability
Open the network tab on your marketing site and count the distinct hostnames it
contacts. Then try to say, for each one, who added it, when, and which of them
you could remove today.
Most people can account for three or four. Ordinary marketing pages routinely
contact more than twice that, and the gap between the list and the memory is
the subject of this piece.
How the list grows
Nobody sits down and decides on eleven domains. They arrive one reasonable
request at a time.
"Add analytics" gets you a tag. "We need to know which ads convert" adds a
second, from a different vendor, which loads its own partner pixel at runtime.
"Add live chat" brings a widget that pulls its own font and its own error
reporter. Someone trials a heatmap tool for a fortnight and the trial
ends but the snippet does not. A tag manager is installed so that marketing can
add tags without engineering, which works exactly as designed: tags get added
without engineering.
None of this needed AI. What a coding agent changes is the friction — the hour
of someone's attention that used to sit between "we should track that" and a
script tag in production, and that was the only brake on the process. Asked to
add analytics, an agent finds the vendor's documented snippet and puts it in
the layout template, which is what the vendor's own quickstart tells it to do.
It will not ask whether the previous three analytics tools are still needed, or
notice that two of them now collect the same events, because it was asked to
add a thing and the thing was added.
You can build a test that fails when a page acquires a new outbound domain. It
takes a browser-based test that inspects network requests, or a
Content-Security-Policy-Report-Only header with somewhere to send the
reports. Neither is on by default in the frameworks we have used, and neither
appears in the change that added the tag.
Six months of that, and the honest answer to "why does the page contact this
host" is that a ticket, once, implied it.
What it costs, in three separate ways
These are usually discussed as one problem. They are not — they have different
owners and different fixes.
Performance. A third-party script is a request to a host you do not
control, resolved through a DNS lookup and a TLS handshake you cannot warm, on
a connection whose latency and availability are somebody else's operational
concern. A synchronous tag in the head blocks parsing. A tag that injects
further tags at runtime is a request chain your build has never seen and your
performance budget cannot account for, because the second and third hops do not
exist until the first one runs. The heavy part is rarely the bytes; it is the
main-thread time spent parsing and executing them — on a low-end Android
handset, not on the machine you tested it on.
Privacy and compliance. Every one of those hosts receives, at minimum, the
visitor's IP address, user agent, and the full URL of the page — which on a
search results page or an account page can carry more than you meant. Under
GDPR that is processing personal data by a third party on your behalf, which
needs a lawful basis, a record, and usually a line in a privacy policy — and
whether the vendor is your processor or a controller in its own right is a
question with real consequences that most teams have never asked about half
their tags. Your consent banner blocks the tags it
knows about. It does not block the pixel that the tag it allowed loaded on its
own initiative. The organisation's stated position and the browser's actual
behaviour drift apart quietly, and it is often somebody outside the
organisation who notices first.
Supply chain. A