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. |
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 |
url |
str
|
Page URL ( |
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 |
settle |
float
|
Seconds to wait after |
click_button |
str | None
|
Accessible name of a button to click after load (trusted CDP
input), for pages that compute their result on demand; |
extractor |
str | None
|
Optional JavaScript function source evaluated with
|
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
|
|
python |
str
|
Python version. |
botonomus_version |
str
|
Package version. |
driver |
str
|
Driver name. |
headless |
bool
|
Whether sessions ran headless. |
browser_product |
str
|
|
executable_name |
str | None
|
File name of the browser executable, or |
executable_sha256 |
str | None
|
SHA-256 of the executable's bytes, or |
proxy_type |
str | None
|
Proxy scheme ( |
botonomus_build |
bool | None
|
Whether the executable is a Botonomus Chromium build, or
|
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 |
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 ( |
error_type |
str | None
|
Exception class name, or |
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 |
failed |
int
|
Runs whose extractor returned |
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
|
|
fail_rate |
float | None
|
|
unknown_rate |
float | None
|
|
blocked_rate |
float | None
|
|
error_rate |
float | None
|
|
mean_duration |
float | None
|
Mean run duration in seconds, or |
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. |
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
¶
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
¶
Return the next posted snapshot. Blocking.
Raises:
| Type | Description |
|---|---|
Empty
|
If none arrives within |
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'
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A JSON-serialisable report with |
dict[str, Any]
|
|
dict[str, Any]
|
|
dict[str, Any]
|
plus |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
parse_levels
¶
Parse a comma-separated list of positive concurrency levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str
|
Text such as |
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'
|
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
One |
should_stop
¶
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 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 |
classify_error
¶
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 |
str
|
|
normalise_verdict
¶
Validate an extractor's return value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
object
|
Whatever the extractor returned. |
required |
Returns:
| Type | Description |
|---|---|
Verdict
|
The verdict ( |
dict[str, Any]
|
|
tuple[Verdict, dict[str, Any]]
|
details object (empty unless a JSON-serialisable dict; replaced by |
tuple[Verdict, dict[str, Any]]
|
|
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 |
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
( |
True
|
output
|
Path | None
|
Directory for screenshots, page text and |
None
|
interact
|
bool
|
Move and scroll with |
True
|
settle
|
float | None
|
Overrides every site's |
None
|
run_timeout
|
float
|
Seconds allowed per run, including launch and close. |
180.0
|
text_limit
|
int
|
Characters kept in |
500
|
seed
|
int | None
|
Seed for interaction paths. |
None
|
Returns:
| Type | Description |
|---|---|
DetectionReport
|
The report. When |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
For invalid |
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, |
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 |
None
|
run_timeout
|
float
|
Seconds per visit. |
180.0
|
factory
|
ManagerFactory | None
|
Builds a manager for one configuration; defaults to a one-slot
|
None
|
Returns:
| Type | Description |
|---|---|
ExperimentReport
|
The report, also written to |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If an arm's options do not form a valid |
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 |
'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 |
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. |