Report on a shop before you automate it
Open a session, run a consent report on a German shop, read what was set before any choice, map where its forms lead, and keep a full-page screenshot.
Last updated:
When a report beats an agent task
You could ask an agent to visit a shop and tell you what its cookie banner does. It would work, cost tokens, take a minute, and give you a slightly different answer every time — because a model wrote the answer. A report is the other shape: one POST, computed from the same perception the agent runs on, with no model anywhere in the call. It is deterministic, it is cheap, and running it again next month produces a document you can diff against this month's.
Reach for a report when the question is structural: what does this page set before anyone chooses, where do its forms post, is a cancellation reachable from here, what does it look like right now. Reach for an agent task when the question needs judgement or several steps — log in, find last quarter's invoice, download it. The two share a session, so you can do both in one browser without paying for two.
This guide runs three of the five reports against one German shop, in order. Everything below needs an API key and a deployment that lists reports in GET /v1/me; the field reference for every parameter is on the reports page.
Step 1: a session, then the consent report
Sessions default to a de-DE browser in Europe/Berlin, which is what a German shop expects to see. Three minutes is plenty for a handful of reports.
Requires: api-key
import { Browserberg } from '@browserberg/sdk';
const bb = new Browserberg({
apiKey: process.env.BROWSERBERG_API_KEY!,
baseUrl: 'https://browserberg.com',
});
// A short session is enough: a report is one call, not a conversation.
await using session = await bb.sessions.create({ ttlSeconds: 180 });
const consent = await session.consentReport({
url: 'https://shop.example.com/',
policy: 'reject',
waitMs: 5000,
});
console.log('platform:', consent.cmp, 'seen after', consent.bannerDetectedAtMs, 'ms');
console.log('cookies before any choice:', consent.summary.cookiesBeforeChoice);
console.log('third-party hosts before any choice:', consent.summary.thirdPartyHostsBeforeChoice);
console.log('new cookies after the reject:', consent.summary.newCookiesAfterReject);
for (const cookie of consent.cookies.beforeChoice) {
if (cookie.thirdParty) console.log(' third party:', cookie.name, cookie.domain);
}
import os
from browserberg import Browserberg
bb = Browserberg(os.environ["BROWSERBERG_API_KEY"], base_url="https://browserberg.com")
with bb.sessions.create(ttl_seconds=180) as session:
consent = session.consent_report(
"https://shop.example.com/", policy="reject", wait_ms=5000
)
print("platform:", consent.cmp, "seen after", consent.banner_detected_at_ms, "ms")
print("cookies before any choice:", consent.summary["cookiesBeforeChoice"])
print("third-party hosts:", consent.summary["thirdPartyHostsBeforeChoice"])
for cookie in consent.cookies_before_choice:
if cookie["thirdParty"]:
print(" third party:", cookie["name"], cookie["domain"])
Reading the consent result
cmp names the platform that answered — usercentrics, cookiebot, consentmanager and borlabs are among the platforms recognised by name — or heuristic when an unrecognised banner was matched by its labels alone, or null when the page showed none. bannerDetectedAtMs is why waitMs exists: several platforms paint their wall a second or two after the page, and a report that looked once would call a site banner-free that every visitor sees a wall on.
The two halves that matter are cookies.beforeChoice and requests.beforeChoice. They are captured before anything is clicked, and the request collector is attached before the navigation, so the third parties a page contacts on load are in the count. summary.thirdPartyHostsBeforeChoice is the single number most people are after. Then summary.newCookiesAfterReject counts what appeared after the reject that was not there before it — a small number is unremarkable, a large one is a finding worth looking at by hand.
Under policy: 'detect-only' nothing on the page is touched and every after-choice half comes back null. Use it when you want to know what is showing without changing the browser's state.
Step 2: where the controls lead, then a picture
The destination report resolves each control in the page rather than guessing from the markup, and runs the destination guard over each one separately.
Requires: api-key
import { writeFile } from 'node:fs/promises';
import { Browserberg } from '@browserberg/sdk';
const bb = new Browserberg({
apiKey: process.env.BROWSERBERG_API_KEY!,
baseUrl: 'https://browserberg.com',
});
await using session = await bb.sessions.create({ ttlSeconds: 180 });
// Where does every control on this page actually lead?
const map = await session.destinationReport({
url: 'https://shop.example.com/',
limit: 200,
});
console.log('page site:', map.anchorSite, '| served over https:', map.secure);
console.log('off-site form posts:', map.summary.offSiteSubmit);
console.log('insecure submits:', map.summary.insecureSubmit);
for (const control of map.controls) {
if (control.risk) {
console.log(control.risk, '|', control.name, '->', control.destination.url);
}
}
// A picture to file beside the numbers. The banner is already gone.
const shot = await session.screenshot({ fullPage: true });
await writeFile('shop.jpg', Buffer.from(shot.base64, 'base64'));
import os
from browserberg import Browserberg
bb = Browserberg(os.environ["BROWSERBERG_API_KEY"], base_url="https://browserberg.com")
with bb.sessions.create(ttl_seconds=180) as session:
page_map = session.destination_report("https://shop.example.com/", limit=200)
print("page site:", page_map.anchor_site, "| served over https:", page_map.secure)
print("off-site form posts:", page_map.summary["offSiteSubmit"])
print("insecure submits:", page_map.summary["insecureSubmit"])
for control in page_map.controls:
if control["risk"]:
print(control["risk"], "|", control["name"], "->", control["destination"]["url"])
shot = session.screenshot(full_page=True)
with open("shop.jpg", "wb") as handle:
handle.write(shot.bytes)
What the numbers mean, and what they are not
A control with risk: 'off_site_submit' is a form on this shop that would post to another registrable site — a newsletter box wired straight to a marketing platform, most often. insecure_submit is an https page submitting over http. Both are refused by the destination guard under its default policy, which is exactly what would happen if an agent tried to use that control, so the report doubles as a preview of what your automation will and will not be allowed to do. off_site_navigation is not blocked by default: leaving the site is ordinary, and an SSO redirect does it on purpose.
The screenshot is the cheapest of the five and useful as evidence beside the numbers: it is taken after the banner has been cleared, so the picture shows the page a visitor sees second, not the wall they see first. fullPage captures the whole document, cut at 16384 CSS pixels.
Four of the five reports are also MCP tools — browser_consent_report, browser_destination_report, browser_cancellation_report and browser_screenshot — so a model in an MCP client can reach for them without you writing any of this. The agent view is deliberately not a tool: a model already has observe, and a second rendering of the same tree would only cost it context. All five need protocol 1.5, and on a deployment that does not offer them the routes are not registered at all, so a call answers not_found rather than failing halfway.
Note · These reports do not grade anybody
A consent report is a list of what the browser held and whom the page contacted. A cancellation report is a statement that a control with a certain label was or was not found, where it led, and what the page behind it asked for. Neither says a site is lawful or unlawful, and nothing on the wire carries a verdict. If you publish a finding, publish the observation and let the reader draw the conclusion.