Event.isTrusted and the activation gate
Every DOM event carries a read-only isTrusted boolean. The engine sets it to true for events it created in response to a real device, and to false for any event constructed and dispatched by script. It is not a property page JavaScript can write, and dispatchEvent has no option to request a trusted event. That makes it the cleanest provenance signal in the platform: a click listener that only ever sees isTrusted === false is watching scripted clicks.
Sitting alongside it is user activation, the browser's internal record that the user has genuinely interacted with the page. Activation is what gates popups, fullscreen requests, clipboard writes, and audio playback. Because only a trusted event grants it, a probe can call an activation-gated API and observe whether the browser allows it - an indirect but very reliable read of the same fact. Scripted clicks never grant activation, so the gated call fails even though a click handler ran.
Automation frameworks that drive a browser through its debugging or automation protocol generate genuinely trusted events, because the input is injected below the JavaScript layer, at the same level the operating system would deliver it. This is the key structural difference between driving a browser and scripting a page, and it is why provenance checks alone do not identify protocol-driven automation - the ordering and physics checks below are what carry that load.
Ordering: real input arrives as a sequence, not an event
A real interaction produces a specific, spec-defined cascade of events. Script that dispatches only the event a handler listens for produces a fragment of that cascade, and the missing members are the signal.
| Interaction | Real sequence | Common scripted shortcut |
|---|---|---|
| Typing a character | keydown then keypress/beforeinput then input then keyup | Set el.value directly - no events at all |
| Clicking a control | pointermove then pointerdown, mousedown, pointerup, mouseup, click | Call el.click() - a click with no pointer history |
| Choosing from a dropdown | input then change, both trusted | Assign select.value, dispatch change only |
The most conclusive member of this family is value without events. Setting input.value from script changes the field contents and fires nothing. A page that watches an input for the full keystroke cascade and then reads a populated field it never saw filled has observed something that cannot happen through a keyboard.
Physics: the durations and distances real hardware produces
The last layer measures the parts of an interaction that come from a human body and a physical device rather than from an event constructor.
- Dwell duration - the time a mouse button or key is held. Real presses last tens of milliseconds; synthesised pairs frequently land in the same millisecond or at a suspiciously fixed interval.
- Approach - a real pointer travels to a control before pressing it, producing
pointermoveevents along a curved path with varying velocity. A click that appears with no prior movement toward the target has no approach. - Landing dispersion - repeated clicks on the same control land on slightly different pixels. Identical coordinates across several interactions indicate a computed target rather than a hand.
- Event cadence - pointer events arrive on the input device's hardware sampling interval. Events spaced on a suspiciously regular software timer do not match any real device rate.
These overlap with behavioural detection, but the framing differs: behavioural models score how human a pattern looks, while provenance checks ask whether the structural preconditions of real input were met at all. The first is statistical and tunable; the second is closer to a proof.
For most public-data collection this whole layer is avoidable rather than solvable. Interaction-heavy flows are the expensive path; where the data is reachable from a request that does not require simulating a person, that path is both cheaper and free of this entire class of signal. A managed web data API that handles session and rendering concerns lets a scraper stay on it.
What a challenge checkbox records
A bot-check checkbox is the clearest case of provenance checking in practice. Public analysis of one widely deployed widget (October 2026) shows its click handler storing at least 60 values as strings: the event's isTrusted, every coordinate system (client, page, screen, layer, offset, movement), the pointer fields (pointerId, pointerType, pressure, tilt, twist), the checkbox's bounding box, the window's scroll and size, devicePixelRatio, the active element, computed style and the element returned by elementFromPoint.
The coordinate systems have to agree: screen coordinates must equal client coordinates plus the iframe's position on screen, and a click whose screen coordinates are relative to the frame instead, a known quirk of protocol-generated clicks in cross-origin frames, contradicts the window geometry. Engine details show too: a WebKit tap reports a random negative pointerId where Chromium reports 1.
Before the click, the widget counts seven input event types: mousemove, pointermove, pointerover, touchstart, click, keydown and wheel. A mouse produces a pointerover and then pairs of pointermove and mousemove on the way to the box; a phone tap produces pointerover, touchstart and a single emulated mousemove. Playwright and Puppeteer click() move the mouse to the exact centre in one step (steps defaults to 1), so the tally sees one move and a centred click, while element.click() fires only an untrusted click with no lead-up. The handler also records new Error().stack; see stack trace inspection.
