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.