TRACIO's bot detection system identifies automated browsers, headless tools, and scripted attacks using a two-layer architecture: client-side signal collection and server-side analysis. The system detects 27+ automation frameworks with near-zero false positives while automatically allowlisting legitimate search engine crawlers.
The browser agent collects 15 bot-specific signals plus 30 cross-validation signals that feed into server-side decisions:
| Signal | Description |
|---|---|
s300 (webdriver) | navigator.webdriver flag — set to true by automation frameworks |
s301 (eval length) | eval.toString().length — Chrome/Edge: 33, Firefox: 37. Other values indicate instrumentation. |
s305 (bot composite) | Scans window/document for 14 automation framework artifacts |
s97 (headless markers) | CDP artifacts, notification permissions, plugin count, outer dimensions |
s157 (automation scan) | Comprehensive scan for Selenium, Puppeteer, PhantomJS, Playwright, and 10 more |
s158 (iframe webdriver) | Creates hidden iframe and reads navigator.webdriver inside it. Catches tools that patch the main frame but forget iframes. |
s159 (native function) | Deletes Function and Object, tests if getters are truly native. Detects Proxy-based instrumentation. |
s155 (window CRC32) | CRC32 hash scan of every window property to detect injected automation globals |
s163 (devtools trap) | Console getter trap — detects when DevTools is actively open |
The server runs 14 independent detectors in priority order. The first matching
detector determines the verdict. Each detector resolves to a bot_result plus,
for bot verdicts, a free-form bot_type label:
Priority Detector Verdict──────── ───────────────── ──────────────────────────1 Webdriver flag bot (type: webdriver)2 Composite bot bot (type: {framework})3 Verified good bot human (search-engine crawler)4 Headless (11 markers) bot (type: headless)5 Eval length anomaly bot (type: unknown)6 Bad bot UA pattern bot (type: {label})7 Automation framework bot (type: {framework})8 VM detection (informational)9 Emulator detection (informational)10 Frida detection (informational)11 DevTools detection (informational)12 Privacy browser (informational)13 Cloned app (informational)14 Behavioral analysis bot (type: rateBot)Bot detection writes a bot_result field on every event (and bot.result in
the webhook payload).
bot_result is one of three canonical values:
| Result | Description |
|---|---|
human | No automation indicators found. Verified search-engine crawlers also resolve to human. |
bot | Automation or scripted access detected. A free-form bot_type label accompanies it. |
uncertain | Mixed or weak signals — neither clearly human nor clearly automated. |
This maps to the business decision field (real, fake, or suspicious)
used across the dashboard. See the Server API Reference
for the full event schema.
"bot")bot_type is a free-form string. The values below are illustrative examples of
what the detectors emit, not an exhaustive enum:
| Type | Detection Method |
|---|---|
webdriver | navigator.webdriver is true |
selenium | Selenium-specific window/document properties detected |
puppeteer | Puppeteer/Chromium headless indicators matched |
playwright | Playwright-specific markers or UA string detected |
headless | 2+ of 11 headless markers triggered |
phantomjs | PhantomJS globals detected (callPhantom, _phantom) |
nightmare | NightmareJS globals detected |
unknown | Eval length anomaly or other non-specific automation indicators |
rateBot | Behavioral rate analysis exceeded thresholds |
The most direct detection. All major automation frameworks set navigator.webdriver = true per the WebDriver specification. This is checked in both the main frame (s300) and a hidden iframe (s158) to catch tools that patch only the main frame.
Comprehensive scanning of window and document properties for framework-specific artifacts:
| Framework | Properties Detected |
|---|---|
| Selenium | _Selenium_IDE_Recorder, _selenium, calledSelenium, __selenium_evaluate, $cdc_* (ChromeDriver) |
| Puppeteer | domAutomation, domAutomationController, HeadlessChrome UA |
| Playwright | __playwright, automation-controlled flag |
| PhantomJS | callPhantom, _phantom |
| NightmareJS | __nightmare, nightmare |
| CefSharp | CefSharp |
| Awesomium | awesomium |
| WebDriverIO | wdioElectron |
| Cypress | __cypress window property |
The scan also uses a regex pattern /^([a-z]){3}_.*_(Array|Promise|Symbol)$/ to detect Selenium's injected helper globals.
11 markers are evaluated, with a threshold of 2 or more for classification:
| Marker | Signal | Condition |
|---|---|---|
| webdriver | s300 | = "true" |
| missing vendor flavors | s28 | No chrome/safari objects on window |
| CDP headless chrome | s305 | headlessChrome = true |
| notification denied | s88 | Notification.permission = "denied" on first visit |
| chrome no runtime | s89 | window.chrome exists but chrome.runtime is missing |
| single language | s90 | navigator.languages.length <= 1 |
| default screen 800x600 | s5 | Headless Chrome default resolution |
| HeadlessChrome UA | UA | User-Agent contains "HeadlessChrome" |
| zero outer dimensions | s97 | outerWidth = 0 and outerHeight = 0 |
| WebGL SwiftShader | s70 | Renderer contains "swiftshader" (software GPU) |
| WebGL llvmpipe | s70 | Renderer contains "llvmpipe" (software GPU) |
22 good bot types are recognized by User-Agent pattern. Each is verified via reverse DNS to prevent spoofing:
.googlebot.com)| Bot | Allowed Domains |
|---|---|
.googlebot.com, .google.com | |
| Bing | .search.msn.com |
| Apple | .applebot.apple.com |
| Yahoo | .crawl.yahoo.net |
| Yandex | .yandex.com, .yandex.net, .yandex.ru |
| DuckDuckGo | .duckduckgo.com |
If a request claims to be Googlebot but the IP does not resolve to *.googlebot.com, it is classified as bot with type googleFakeBot.
Rate-based detection with entropy scoring:
| Rate (req/min) | Entropy Threshold | Action |
|---|---|---|
| 120+ | < 0.1 | bot (type: rateBot) |
| 60-120 | < 0.3 | bot (type: rateBot) |
| 45-60 | < 0.1 | bot (type: rateBot) |
| < 45 | Any | Normal |
Additional rule: No mouse/keyboard events after 30+ seconds session age classifies the visit as bot with type noInteraction.
eval.toString().length produces consistent values per engine:
Any other value indicates the eval function has been hooked by an automation framework, classifying the visit as bot with type unknown.
The client SDK exposes a convenience boolean, result.bot.detected:
const result = await tracio.getResult()
if (result.bot.detected) { // Log and block console.warn(`Bot detected (confidence ${result.bot.confidence})`) showCaptcha() return}
// Proceed with loginawait login(credentials)On your server, fetch the event from the Server API. The
event is a flat object — bot detection lives in the bot_result and bot_type
fields:
// `data` is the event object from GET /subscriptions/:id/events/:requestIdconst { data: event } = await fetchEvent(requestId)
if (event.bot_result === "bot") { await db.blockedRequests.insert({ visitorId: event.visitor_id, botType: event.bot_type, ip: event.ip, timestamp: new Date(), })
return res.status(403).json({ error: "Automated access detected" })}TRACIO's bot detection is designed for near-zero false positives. However, edge cases can occur:
| Scenario | Risk | Mitigation |
|---|---|---|
Browser extensions modifying navigator | Low | Multiple signal cross-validation prevents single-signal triggers |
| Corporate security software | Very Low | Headless detection requires 2+ markers |
| Accessibility tools | None | Accessibility APIs do not trigger any detection signals |
| VPN/proxy users | None | VPN usage is tracked separately from bot detection |
If you encounter false positives, review the bot.type field to understand which detector was triggered and adjust your policy accordingly.