Every technique in this area presupposes a list. Consent categories are assigned per vendor; content security policies allowlist origins; budgets are attributed to owners; isolation decisions are made script by script. All of it depends on knowing what is actually on the page — and on most production sites, nobody does.
The reason is structural rather than negligent. Tags arrive through a tag manager without a commit, vendors load their own dependencies at runtime, marketing campaigns add pixels for a fortnight and never remove them, and the person who requested a given script left two years ago. A repository search finds the scripts your code loads; it does not find the ones your scripts load.
An inventory built from observed traffic rather than from source is the artefact that closes this gap. This guide covers how to collect one, what each entry needs to be useful, and how to detect the drift that will otherwise return the list to fiction within a quarter.
What an Inventory Entry Needs
A list of hostnames is not an inventory — it is a starting point that nobody can act on. Each entry needs four things beyond the origin, and the value of the whole exercise is in the fourth.
The observed cost: bytes transferred and main-thread time attributable to it, drawn from the same vendor attribution that feeds your RUM. The loading path: whether it was declared in your markup, injected by your code, or loaded by another vendor — the third case is the one that surprises people. The consent category it has been assigned, which links it to the gate. And an owner: a named team that will answer for it.
The owner field is what makes an inventory more than a report. A vendor with a cost and no owner cannot be removed, because nobody can say whether removal breaks something — so it stays, permanently, and the list grows. Requiring an owner at the point of addition is the only mechanism that reliably keeps the list from becoming a museum.
Collecting the List from Real Traffic
There are three collection surfaces and they see different things, which is why a serious inventory uses more than one. Each has a blind spot the others cover.
Resource timing from live sessions is the most representative: it observes what real visitors on real routes actually loaded, including vendors that only appear for some cohorts. Its blind spot is cross-origin entries without Timing-Allow-Origin, which report zero sizes — present, but uncosted.
A synthetic crawl with a headless browser sees every route you point it at, including ones with low traffic that field collection may never sample. Its blind spot is anything conditional on a real user: consent-gated tags, logged-in state, geography, and experiment variants.
Tag-manager configuration export is the only surface that shows tags which are configured but not currently firing — the ones waiting on a trigger that will eventually match. Its blind spot is everything the tags themselves load at runtime.
// collect-inventory.js — the field half. Ship with your RUM sampling.
function collectThirdParty() {
const firstParty = location.host;
const seen = new Map();
for (const e of performance.getEntriesByType('resource')) {
const host = new URL(e.name, location.href).host;
if (host === firstParty) continue;
const row = seen.get(host) || { host, bytes: 0, count: 0, uncosted: 0 };
row.count += 1;
// transferSize is 0 both for cache hits and for cross-origin responses
// without Timing-Allow-Origin. Track those separately rather than as free.
if (e.transferSize > 0) row.bytes += e.transferSize;
else row.uncosted += 1;
seen.set(host, row);
}
return [...seen.values()];
}
Detecting Drift
A list is accurate on the day it is compiled. What keeps it accurate is a diff, run automatically, that reports three specific events: a new origin appearing, a known origin’s cost changing materially, and a known origin ceasing to appear.
The third is as valuable as the first. An origin that stops appearing is either a vendor that was removed and can be deleted from the allowlist, the consent categories and the policy — or a vendor that is silently failing to load. Both need a decision, and neither surfaces without the check.
// diff-inventory.js — run on a schedule, compare against the committed list.
export function diff(previous, current) {
const before = new Map(previous.map((r) => [r.host, r]));
const after = new Map(current.map((r) => [r.host, r]));
const added = [...after.keys()].filter((h) => !before.has(h));
const removed = [...before.keys()].filter((h) => !after.has(h));
// Material change only — small variation is normal and should not alert.
const grew = [...after.values()].filter((r) => {
const was = before.get(r.host);
return was && was.bytes > 0 && r.bytes / was.bytes > 1.25;
});
return { added, removed, grew };
}
Route the three categories differently. A new origin is a security and compliance event: it may be an unauthorised tag, and it certainly is not in the consent map or the CSP allowlist yet, so it should page someone. Growth is a performance event that belongs in a weekly review rather than an alert. Disappearance is a housekeeping event that should open a ticket to confirm and clean up.
From Inventory to Decision
A list that is never acted on decays into a report nobody opens, so it is worth being concrete about what the inventory is for. Three recurring decisions consume it, and each wants the data sliced differently.
The removal conversation. Sort by cost and read the top ten with their owners present. The question is not “is this vendor useful” — every vendor is useful to someone — but “is it worth what the median visitor pays for it”. Framing it as a trade with a named number changes the discussion: a team defending a 180 ms tag against a 200 ms budget is negotiating for a share of a fixed resource, not asserting a preference. Most sites find one or two vendors in the top ten that nobody actively wants, which is the cheapest performance work available anywhere.
The consolidation conversation. Group by what the vendor does rather than by who asked for it, and duplicates surface immediately: two analytics products, three tools that all record sessions, a tag manager loading a second tag manager. Duplication of this kind is nearly always organisational rather than technical — two teams each procured a tool for the same job — and the inventory is what makes it visible, because neither team can see the other’s line item any other way.
The onboarding gate. The most valuable use of the inventory is preventing additions rather than removing them. A change that adds a vendor should have to add a row: origin, category, owner, expected cost, and what it replaces. Requiring the row at the point of addition is a much lighter process than a review board, and it catches the two failure modes that matter — a vendor added with no owner, and a vendor added without anyone checking whether an existing one already does the job.
Each of these depends on the list being trusted, which in turn depends on it being generated rather than curated. The moment the inventory is something a person maintains by hand it starts to lag reality, and a list that is known to lag is a list that gets argued with rather than acted on. Keep the collection automatic, keep the annotations — owner, category, justification — in the same file under version control, and the artefact stays credible enough to make decisions from.
Verification Checklist
Starting From Nothing
If no inventory exists today, the first version does not need to be complete to be useful. Collect a week of field resource timing, group by registered domain, sort by bytes, and take the top twenty. That list will already contain every vendor anyone argues about, and producing it takes an afternoon.
Resist the urge to make version one exhaustive. The trailing set of one-request origins — a font, a favicon service, a single pixel — is real but rarely decision-relevant, and chasing it delays the conversation the top twenty makes possible. Add the owner column by asking, in one message, who owns each of those twenty; the entries nobody claims are your first findings, before any measurement is interpreted at all.
Once the list exists and has been used once to make a decision, automating the collection becomes an easy sell, because the value is no longer hypothetical. Attempting the automation first, with no demonstrated use, is how inventory projects become dashboards nobody opens.
Interaction Matrix
| Pattern | How the inventory feeds it |
|---|---|
| Consent gating | The category column is the gate’s configuration. A vendor absent from the inventory is a vendor absent from the gate. |
| Strict CSP | The origin column generates the allowlist, and the diff keeps it from accumulating dead entries. |
| Subresource integrity | The same registry holds the digests; the inventory records which vendors could not be pinned. |
| Budget enforcement | Per-vendor budgets are derived from observed cost, and the owner column tells the gate whom to notify. |
| RUM attribution | Shares the vendor registry outright — the attribution’s unmatched bucket is the inventory’s discovery queue. |
Keeping the List Honest Over Time
Two habits keep an inventory from drifting back into fiction. The first is to store it in the repository rather than in a dashboard, so every change to it is a diff with an author and a review — which turns “a new vendor appeared” from a notification someone dismisses into a pull request someone approves.
The second is to record why each entry exists, in a sentence, next to the owner. Cost and category describe what a vendor does; the justification describes what it is for, and it is the only field that ages informatively. A justification that no longer reads as convincing two years later is the clearest signal available that a vendor has outlived its purpose, and it is a far easier conversation to have than one that starts from a number.
Neither habit costs anything at collection time, and both are difficult to add retrospectively — the justifications in particular, because the people who knew them have usually moved on by the time anyone wants them.
Troubleshooting
The field list and the crawl disagree substantially. Expected, and informative: the difference is usually consent-gated tags the crawl never triggers, plus experiment variants. Treat the union as the inventory and record which surface saw each entry.
Many origins report zero bytes. Those are cross-origin responses without Timing-Allow-Origin, not free resources. Request the header from the vendor; where it is not forthcoming, size them from a synthetic run and mark the figure as estimated.
The list churns constantly with CDN shard hostnames. Normalise before diffing: collapse a1.cdn.vendor.com and a2.cdn.vendor.com to the registered domain, and diff on the vendor key rather than the raw host.
A vendor appears only in some sessions. That is a real finding, not noise — it is geography, an experiment, or a consent branch. Segment the collection by those dimensions rather than aggregating them away.
Nobody claims an entry. Default to removal with a scheduled date, announced in advance. An unclaimed vendor that nobody misses after a fortnight has answered the ownership question.
Frequently Asked Questions
How is this different from Lighthouse's third-party summary?
The Lighthouse audit is a single synthetic run against one URL, which makes it an excellent starting point and a poor inventory. It cannot see consent-gated vendors, authenticated routes, regional variants, or anything that loads for a cohort it did not emulate — and it has no notion of ownership or of change over time.
Use it to bootstrap the list and to spot-check, then move to field collection for the version you act on. The two agreeing is reassuring; the two disagreeing is where the interesting vendors are.
How often should the inventory be reviewed by humans?
The automated diff runs continuously; the human review is for the questions a diff cannot answer — is this vendor still earning its cost, is the owner still the right owner, has the consent category drifted from what the vendor actually does now. Quarterly is a reasonable default, aligned with whatever privacy review you already run so it is one meeting rather than two.
Time-box it by reviewing the top entries by cost rather than the whole list. The trailing set of small vendors matters in aggregate but rarely repays individual attention, whereas the top five usually account for most of what a visitor pays.
Should the inventory be public?
A version of it usually already is, or should be: privacy regulations expect a cookie or vendor disclosure listing who receives data and why, and that disclosure is only accurate if it is derived from the same source as the technical configuration. Generating both from one registry is how they stay in agreement.
The public version is a subset — vendor names, purposes, retention — rather than the operational one with owners and byte counts. Keeping them in one file with a flag per field is simpler than maintaining two lists that will diverge.
What is a realistic number of third-party origins?
Fewer than you have. There is no correct figure, but the distribution is instructive: most sites can name three or four vendors they would defend, and most sites load considerably more than that. The gap between those two numbers is the inventory’s whole reason for existing.
Rather than targeting a count, target a property: every origin has an owner, a category, and a cost someone has looked at within the last quarter. Sites that hold that line tend to converge on a small number naturally, because each addition has to survive a conversation.
One closing observation: the inventory tends to be most valuable to the people who did not build it. Security teams, privacy counsel and procurement all need a current list of who receives data, and none of them has any other way to obtain one. Publishing it internally, on a schedule, converts an engineering artefact into a shared one — and makes the case for keeping it accurate far easier to sustain.