Skip to content

botonomus.diagnostics

Local probe, detection-site runner and load benchmark.

botonomus.diagnostics

Local realism diagnostics, detection-site runs and load measurement.

Observations here are evidence about one machine, browser and date, never a guarantee that any website will treat a session as human.

CATALOGUE module-attribute

CATALOGUE: Final[tuple[DetectionSite, ...]] = (DetectionSite('deviceandbrowserinfo', 'https://deviceandbrowserinfo.com/are_you_a_bot', ready=f'/you are (a bot|(a )?human)/i.test({_BODY_TEXT})', extractor=_DEVICEANDBROWSERINFO, description='Bot/human statement from fingerprint and CDP side-effect checks'), DetectionSite('sannysoft', 'https://bot.sannysoft.com/', ready="document.querySelectorAll('td.passed, td.failed').length > 10", extractor=_SANNYSOFT, description='Classic headless and webdriver property table'), DetectionSite('browserscan', 'https://www.browserscan.net/bot-detection', settle=10.0, extractor=_BROWSERSCAN, description='Bot detection summary (Normal / Robot)'), DetectionSite('fingerprint', 'https://demo.fingerprint.com/web-scraping', settle=12.0, extractor=_FINGERPRINT, description='Fingerprint Pro bot and tampering decision on a scraping demo'), DetectionSite('fingerprint-playground', 'https://demo.fingerprint.com/playground', settle=15.0, extractor=_FINGERPRINT_PLAYGROUND, description='Fingerprint Pro Smart Signals: tampering, anti-detect browser, scores'), DetectionSite('creepjs', 'https://abrahamjuliot.github.io/creepjs/', settle=20.0, extractor=_CREEPJS, description='Headless, like-headless and stealth percentages'), DetectionSite('incolumitas', 'https://bot.incolumitas.com/', settle=12.0, extractor=_INCOLUMITAS, description='OK/FAIL detection test tables (spec-level WEBDRIVER ignored)'), DetectionSite('nowsecure', 'https://nowsecure.nl/', extractor=_NOWSECURE, description='Cloudflare challenge page'), DetectionSite('recaptcha-score', 'https://antcpt.com/score_detector/', ready=f'/score is:?\\s*[01]/i.test({_BODY_TEXT})', extractor=_ANTCPT, description='reCAPTCHA v3 score (pass >= 0.7, fail <= 0.3)'), DetectionSite('pixelscan', 'https://pixelscan.net/fingerprint-check', settle=20.0, extractor=_PIXELSCAN, description='Fingerprint consistency and masking'), DetectionSite('fingerprint-scan', 'https://fingerprint-scan.com/', settle=15.0, extractor=_FINGERPRINT_SCAN, description='Bot score out of 100 (pass <= 30, fail >= 60)'), DetectionSite('rebrowser', 'https://bot-detector.rebrowser.net/', settle=10.0, extractor=_REBROWSER, description='Automation leak tests (Runtime.enable, init scripts, UA)'), DetectionSite('turnstile', 'https://nopecha.com/demo/turnstile', settle=15.0, extractor=_TURNSTILE, description='Cloudflare Turnstile demo'), DetectionSite('apivoid', 'https://www.apivoid.com/tools/bot-detection-test/', settle=15.0, extractor=_APIVOID, description='Server-side risk score and tampering verdict (pass <= 30, fail >= 70)'), DetectionSite('donutbrowser', 'https://donutbrowser.com/tools/bot-detection/', settle=10.0, extractor=_DONUTBROWSER, description='Automation and spoofing checks, bot score out of 100'), DetectionSite('cleantalk', 'https://cleantalk.org/am-i-a-bot', settle=10.0, extractor=_CLEANTALK, description='Human score out of 100 (pass >= 80, fail < 50)'), DetectionSite('pixelscan-bot', 'https://pixelscan.net/bot-check', settle=15.0, extractor=_PIXELSCAN_BOT, description='Bot check: navigator, webdriver, CDP and user-agent parameters'), DetectionSite('recaptcha-google', 'https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php', ready=f'/"score"/.test({_BODY_TEXT})', ready_timeout=40.0, settle=1.0, extractor=_RECAPTCHA_JSON, description="Google's reCAPTCHA v3 demo score (pass >= 0.7, fail <= 0.3)"), DetectionSite('recaptcha-2captcha', 'https://2captcha.com/demo/recaptcha-v3', settle=2.0, click_button='Check', ready=f'/"score":/.test({_BODY_TEXT})', ready_timeout=40.0, extractor=_RECAPTCHA_JSON, description='reCAPTCHA v3 demo score after clicking Check'), DetectionSite('turnstile-capskip', 'https://capskip.com/captcha-demo/cloudflare-turnstile/', settle=15.0, extractor=_TURNSTILE_CAPSKIP, description='Cloudflare Turnstile widget verification'))

Built-in public detection sites, in default run order.

SITES module-attribute

SITES: Final[dict[str, DetectionSite]] = {site.name: site for site in CATALOGUE}

The built-in catalogue by name.

DetectionReport dataclass

DetectionReport(environment: Environment, sites: list[SiteSummary], runs: list[RunResult], limitation: str = LIMITATION)

A complete detection run.

Attributes:

Name Type Description
environment Environment

Run metadata.

sites list[SiteSummary]

One summary per site, in request order.

runs list[RunResult]

Every individual run.

limitation str

How (not) to read the results.

to_dict

to_dict() -> dict[str, Any]

The report as JSON-compatible data.

to_json

to_json() -> str

The report as indented JSON text.

DetectionSite dataclass

DetectionSite(name: str, url: str, wait_until: WaitStrategy = 'load', ready: str | None = None, ready_timeout: float = 30.0, settle: float = 8.0, extractor: str | None = None, description: str = '', click_button: str | None = None)

One detection page and how to read it.

Attributes:

Name Type Description
name str

Short identifier: lowercase letters, digits and -, at most 32.

url str

Page URL (http or https).

wait_until WaitStrategy

Navigation lifecycle event to wait for.

ready str | None

Optional JavaScript expression polled (in the isolated world) until truthy, for pages that compute results after load. Not reaching it is not an error; the extractor then decides what it can.

ready_timeout float

Seconds to poll ready.

settle float

Seconds to wait after ready before capturing, with no CDP traffic.

click_button str | None

Accessible name of a button to click after load (trusted CDP input), for pages that compute their result on demand; None for none.

extractor str | None

Optional JavaScript function source evaluated with page.evaluate, which runs in the isolated world on the native and Patchright drivers. It must return {"verdict": "pass"|"fail"|"unknown"| "blocked", "details": {...}}, where "blocked" means the page refused the visit before deciding (rate limit or challenge interstitial); any other shape is recorded as "unknown".

description str

What the page tests, for humans.

Raises:

Type Description
ConfigurationError

On construction, if any value is invalid.

Environment dataclass

Environment(captured_at: str, os: str, python: str, botonomus_version: str, driver: str, headless: bool, browser_product: str, executable_name: str | None, executable_sha256: str | None, proxy_type: str | None, botonomus_build: bool | None = None)

Where and how a report was produced. Never contains credentials or hosts.

Attributes:

Name Type Description
captured_at str

UTC ISO 8601 timestamp.

os str

platform.platform().

python str

Python version.

botonomus_version str

Package version.

driver str

Driver name.

headless bool

Whether sessions ran headless.

browser_product str

Browser.getVersion product, or "unknown".

executable_name str | None

File name of the browser executable, or None.

executable_sha256 str | None

SHA-256 of the executable's bytes, or None.

proxy_type str | None

Proxy scheme (http, https, socks5) or None.

botonomus_build bool | None

Whether the executable is a Botonomus Chromium build, or None when no executable was found.

RunResult dataclass

RunResult(site: str, run: int, profile: str, verdict: Verdict = 'unknown', details: dict[str, Any] = dict(), duration: float = 0.0, error: str | None = None, error_type: str | None = None, screenshot: str | None = None, text_excerpt: str = '')

The outcome of one visit to one site.

Attributes:

Name Type Description
site str

Site name.

run int

1-based run number for this site.

profile str

Profile name used.

verdict Verdict

What the extractor returned, or "unknown".

details dict[str, Any]

The extractor's small JSON details object.

duration float

Seconds from profile preparation to session close.

error str | None

Error category (timeout, navigation, startup, profile_in_use, extractor, cleanup, other) or None.

error_type str | None

Exception class name, or None. Messages are never recorded.

screenshot str | None

Screenshot path relative to the output directory, if saved.

text_excerpt str

Start of the page's visible text, whitespace collapsed.

SiteSummary dataclass

SiteSummary(site: str, url: str, runs: int, passed: int, failed: int, unknown: int, blocked: int, errors: int, pass_rate: float | None, fail_rate: float | None, unknown_rate: float | None, blocked_rate: float | None, error_rate: float | None, mean_duration: float | None, error_categories: dict[str, int])

Aggregated outcomes for one site.

Every run lands in exactly one of passed, failed, unknown, blocked or errors; a run with an error counts only as an error. Rates are fractions of runs, rounded to four places, and None when there were no runs.

Attributes:

Name Type Description
site str

Site name.

url str

Site URL.

runs int

Runs attempted.

passed int

Runs whose extractor returned pass.

failed int

Runs whose extractor returned fail.

unknown int

Error-free runs without a decided verdict.

blocked int

Error-free runs the page refused (rate limit, challenge) before any bot decision.

errors int

Runs that raised.

pass_rate float | None

passed / runs.

fail_rate float | None

failed / runs.

unknown_rate float | None

unknown / runs.

blocked_rate float | None

blocked / runs.

error_rate float | None

errors / runs.

mean_duration float | None

Mean run duration in seconds, or None.

error_categories dict[str, int]

Count per error category.

ExperimentReport dataclass

ExperimentReport(arms: dict[str, dict[str, Any]], sites: list[str], summaries: list[ArmSiteSummary], runs: list[ExperimentRun], environments: dict[str, Environment], limitation: str = LIMITATION)

A complete experiment.

Attributes:

Name Type Description
arms dict[str, dict[str, Any]]

Arm name to its options, JSON-compatible.

sites list[str]

Site names.

summaries list[ArmSiteSummary]

One per (arm, site), arm-major in spec order.

runs list[ExperimentRun]

Every visit in execution order.

environments dict[str, Environment]

Arm name to the environment of its browser.

limitation str

How (not) to read the results.

to_dict

to_dict() -> dict[str, Any]

The report as JSON-compatible data.

to_json

to_json() -> str

The report as indented JSON text.

to_markdown

to_markdown() -> str

One table per site: arm, runs, pass, fail, blocked, unknown, errors, rate, CI.

ExperimentSpec dataclass

ExperimentSpec(sites: tuple[DetectionSite, ...], arms: tuple[Arm, ...], runs: int)

What to run.

Attributes:

Name Type Description
sites tuple[DetectionSite, ...]

Sites, in file order.

arms tuple[Arm, ...]

Arms, in file order.

runs int

Visits per (arm, site).

ProbeServer

ProbeServer()

Serves the probe page on an unguessable loopback route and collects snapshots.

Use as a context manager; url is valid while entered.

Attributes:

Name Type Description
url

The probe page URL.

receive

receive(timeout: float = 30) -> dict[str, Any]

Return the next posted snapshot. Blocking.

Raises:

Type Description
Empty

If none arrives within timeout seconds.

measure_level async

measure_level(level: int, config: BrowserConfig, url: str, *, mode: Mode = 'sessions') -> dict[str, Any]

Open up to level sessions (or contexts) one after another and hold them open.

Each one navigates to url. Host memory is sampled throughout.

Parameters:

Name Type Description Default
level int

Sessions or contexts to hold simultaneously.

required
config BrowserConfig

Configuration for every session (or for the one shared browser).

required
url str

Page each one loads; a local page keeps the network out of it.

required
mode Mode

sessions (a browser per session, as Botonomus.open) or contexts (one browser, an in-memory context per tab, as Client).

'sessions'

Returns:

Type Description
dict[str, Any]

A JSON-serialisable report with requested, mode, active,

dict[str, Any]

failures, stopped_early, baseline_host_used_bytes,

dict[str, Any]

peak_host_used_bytes, per_instance_bytes and startup_seconds,

dict[str, Any]

plus error_categories when launches failed.

Raises:

Type Description
ValueError

For an unknown mode.

parse_levels

parse_levels(value: str) -> list[int]

Parse a comma-separated list of positive concurrency levels.

Parameters:

Name Type Description Default
value str

Text such as "1,2,5,10".

required

Returns:

Type Description
list[int]

The levels in the given order.

Raises:

Type Description
ValueError

If any part is not a positive integer.

run_benchmark async

run_benchmark(levels: list[int], config: BrowserConfig, url: str, *, mode: Mode = 'sessions') -> list[dict[str, Any]]

Measure each level in turn, stopping after a level that hit a guard.

Parameters:

Name Type Description Default
levels list[int]

Concurrency levels, measured in order.

required
config BrowserConfig

Configuration for every session.

required
url str

Page each session loads.

required
mode Mode

sessions or contexts; see measure_level.

'sessions'

Returns:

Type Description
list[dict[str, Any]]

One measure_level report per measured level.

should_stop

should_stop(available_bytes: int, total_bytes: int, failures: int) -> bool

Whether to stop opening sessions: under 15% memory free, or two failures.

Parameters:

Name Type Description Default
available_bytes int

Host memory currently available.

required
total_bytes int

Total host memory.

required
failures int

Launch failures so far at this level.

required

compare_snapshots

compare_snapshots(manual: dict[str, Any] | None, automated: dict[str, Any]) -> dict[str, Any]

Compare identical probes from the same browser version and environment.

aggregate

aggregate(sites: Sequence[DetectionSite], runs: Sequence[RunResult]) -> list[SiteSummary]

Summarise runs per site.

Parameters:

Name Type Description Default
sites Sequence[DetectionSite]

Sites in report order.

required
runs Sequence[RunResult]

Runs for any of those sites.

required

Returns:

Type Description
list[SiteSummary]

One SiteSummary per site.

classify_error

classify_error(exc: BaseException) -> str

Map an exception to a coarse, driver-independent category.

Parameters:

Name Type Description Default
exc BaseException

The exception a run raised.

required

Returns:

Type Description
str

One of timeout, navigation, startup, profile_in_use,

str

extractor, cleanup or other.

normalise_verdict

normalise_verdict(raw: object) -> tuple[Verdict, dict[str, Any]]

Validate an extractor's return value.

Parameters:

Name Type Description Default
raw object

Whatever the extractor returned.

required

Returns:

Type Description
Verdict

The verdict ("unknown" unless exactly "pass", "fail" or

dict[str, Any]

"blocked") and the

tuple[Verdict, dict[str, Any]]

details object (empty unless a JSON-serialisable dict; replaced by

tuple[Verdict, dict[str, Any]]

{"truncated": True} when larger than 4000 characters as JSON).

run_detection async

run_detection(bot: SessionOpener, sites: Sequence[DetectionSite], *, runs: int = 1, profile_prefix: str = 'detect', fresh_profiles: bool = True, output: Path | None = None, interact: bool = True, settle: float | None = None, run_timeout: float = 180.0, text_limit: int = 500, seed: int | None = None) -> DetectionReport

Visit every site runs times and aggregate the extractors' verdicts.

Sites run concurrently, up to the manager's capacity; the runs of one site run one after another. A failing run is recorded and never stops the others.

Parameters:

Name Type Description Default
bot SessionOpener

An entered manager (or anything with config and open).

required
sites Sequence[DetectionSite]

Sites to visit; names must be unique.

required
runs int

Visits per site.

1
profile_prefix str

Profile name prefix: 1-20 lowercase letters, digits or -.

'detect'
fresh_profiles bool

Delete and recreate a profile per run (<prefix>-<site>-<run>). Otherwise each site reuses <prefix>-<site>, keeping cookies between runs.

True
output Path | None

Directory for screenshots, page text and report.json; nothing is written when None.

None
interact bool

Move and scroll with Human after loading, since some scores weigh pointer activity.

True
settle float | None

Overrides every site's settle seconds.

None
run_timeout float

Seconds allowed per run, including launch and close.

180.0
text_limit int

Characters kept in text_excerpt.

500
seed int | None

Seed for interaction paths.

None

Returns:

Type Description
DetectionReport

The report. When output is given it is also saved as report.json.

Raises:

Type Description
ConfigurationError

For invalid runs, prefix or duplicate site names.

load_spec

load_spec(path: Path, *, runs: int | None = None) -> ExperimentSpec

Read an experiment TOML file. See parse_spec.

Raises:

Type Description
ConfigurationError

For invalid TOML or content.

OSError

If the file cannot be read.

run_experiment async

run_experiment(spec: ExperimentSpec, *, output: Path, proxies: Sequence[str] = (), seed: int | None = None, interact: bool = True, settle: float | None = None, run_timeout: float = 180.0, factory: ManagerFactory | None = None) -> ExperimentReport

Run every scheduled visit one at a time and summarise per (arm, site).

Parameters:

Name Type Description Default
spec ExperimentSpec

The experiment.

required
output Path

Directory for profiles, screenshots, text, report.json and report.md.

required
proxies Sequence[str]

Proxy URLs used round-robin, one per visit; empty for none.

()
seed int | None

Seed for the schedule and interaction paths.

None
interact bool

Move and scroll after loading.

True
settle float | None

Overrides every site's settle seconds.

None
run_timeout float

Seconds per visit.

180.0
factory ManagerFactory | None

Builds a manager for one configuration; defaults to a one-slot Botonomus.

None

Returns:

Type Description
ExperimentReport

The report, also written to output.

Raises:

Type Description
ConfigurationError

If an arm's options do not form a valid BrowserConfig (checked for every arm before the first visit).

automated_snapshot async

automated_snapshot(config: BrowserConfig, server: ProbeServer, *, profile: str = 'probe', timeout: float = 30) -> dict[str, Any]

Open one session under config, load the probe and return its snapshot.

Parameters:

Name Type Description Default
config BrowserConfig

Session configuration (driver, executable, proxy, ...).

required
server ProbeServer

A running probe server.

required
profile str

Profile name under config.profile_root.

'probe'
timeout float

Seconds to wait for the snapshot after navigation.

30

Returns:

Type Description
dict[str, Any]

The version-1 snapshot the probe posted.

Raises:

Type Description
Empty

If no snapshot arrives in time.

BotonomusError

If the session cannot be opened.

normal_snapshot async

normal_snapshot(executable: Path, profile_dir: Path, server: ProbeServer, *, timeout: float = 30) -> dict[str, Any]

Launch executable like a user would and return the probe's snapshot.

The browser gets no debugging port and receives no automation commands; it only gets a throwaway profile and the probe URL.

Parameters:

Name Type Description Default
executable Path

Browser executable.

required
profile_dir Path

Empty directory used as --user-data-dir.

required
server ProbeServer

A running probe server.

required
timeout float

Seconds to wait for the snapshot.

30

Returns:

Type Description
dict[str, Any]

The version-1 snapshot the probe posted.

Raises:

Type Description
Empty

If no snapshot arrives in time.

OSError

If the browser cannot be started.