Skip to content

Latest commit

 

History

History
211 lines (161 loc) · 10.9 KB

human-mouse-movement.md

File metadata and controls

211 lines (161 loc) · 10.9 KB
title Human-like mouse movement: Bezier curves are the easy part
description Human-like mouse movement needs more than a Bezier curve: every pointer event carries fields saying where it came from, and a perfect curve can still fail.
parent The Automation Layer
grand_parent Guides
nav_order 5

Human-like mouse movement: Bezier curves are the easy part

Human-like mouse movement takes two things, not one: a natural path, and pointer events whose fields match a real device. The curve is the easy half. A convincing arc delivered as events that report, in plain fields any page can read, that no hand produced them still fails on the cheapest possible check.

Search for this and you get a dozen libraries that draw a nice curve between two points, with jitter, overshoot and a bell-shaped velocity profile. They are not wrong. They are solving the visible half of the problem.

The half nobody mentions is that a path is made of events, and an event carries fields that say where it came from. You can draw a perfect human arc and still deliver it as a hundred events that no hand ever produced.

What a real pointer event carries

Move a real mouse and each event arrives with more than coordinates:

Field On a real mouse On a naive synthetic event
isTrusted true false if dispatched from page JavaScript
pointerType "mouse" often absent, or "pen" / "touch" by accident
pressure 0 when hovering, 0.5 while a button is down frequently 0 always, or 1 always
buttons a bitmask consistent with the sequence often inconsistent across down, move and up
movementX / movementY the delta the OS reported zero, or recomputed and not matching
timestamps irregular, tied to the input device's rate perfectly spaced

A page can read every one of these. Checking isTrusted alone is one line, and it separates "the driver moved the pointer" from "a script pretended to".

The three layers, and what each can reach

Mouse movement can be produced at three layers, and each reaches less of the event than the one below it: page JavaScript can shape the path but never the trusted flag, the automation driver produces trusted events but not all of their content, and only the browser's own input path decides every field the way a real device would.

Page JavaScript. element.dispatchEvent(new MouseEvent('mousemove', ...)). The path can be as human as you like and the events are isTrusted: false, permanently, because the flag is set by the browser and is not writable by page script. This is the layer most "humanize" snippets on the web operate at, and it fails the cheapest possible check.

The automation driver. page.mouse.move() in Playwright, and the equivalents elsewhere. These produce genuinely trusted events, because the browser generates them internally rather than the page. This is a real improvement and it is where the Bezier libraries plug in: they call mouse.move() many times along a curve instead of once at the destination.

What the driver does not fix by itself is the content of those events. Whether pressure changes when a button goes down, whether pointerType is set, whether the timing between events looks like a device rather than a loop, all depend on what the driver fills in.

The browser's own input path. Values decided where the browser builds the event. At this level pressure, pointerType and the trusted flag are simply what a real device would have produced, because they are produced in the same place.

Why the curve alone is not enough, and why it still matters

Be fair to the curve libraries: straight-line teleporting is a real tell and they fix it. A pointer that jumps from (0,0) to a button and clicks, with no intermediate events at all, is trivially distinguishable from a hand.

But consider what a behavioural check can compare, once you have a curve:

  • Event rate. A real mouse reports at a device-determined rate, so intervals cluster and jitter around it. A loop with await sleep(10) produces a distribution no hardware makes.
  • Sub-pixel behaviour. Real movement produces fractional coordinates on scaled displays. Integer-only paths are a signature.
  • What happens between actions. Humans move while reading, drift, and overshoot targets they are not going to click. Automation usually moves only when it needs to arrive somewhere.
  • The idle case. A session with no pointer events at all before a click is louder than any curve shape.

So the curve is necessary and not sufficient, which is the same shape as every other part of this subject.

The same three layers, measured on one page

Three panels of one target page, each drawing a dot per pointer event received. A single driver move leaves two events and no path; a driver move with forty steps leaves an evenly spaced line with three distinct movementX values; the patched engine's path is unevenly spaced with twenty-six. All three report isTrusted true.

One page served from 127.0.0.1, one target, three ways of reaching it. The page draws a dot per pointermove and prints the fields it actually received, so the picture and the numbers come from the same run.

The first thing the panels settle is a negative: isTrusted is true in all three, and so isTrusted does not separate the driver from the engine. It only separates both of them from page script. A check that stops at the trusted flag has not distinguished anything here.

What does separate them is the deltas. The evenly stepped driver path produces three distinct movementX values across 41 events, because a straight line walked in equal steps reports the same delta almost every time. The engine's path produces 26 distinct values across 34 events. Same trusted flag, same pointerType, same pressure, and a completely different distribution underneath.

And notice what the third panel is not: it is not a graceful arc. The dots stay roughly on the line to the target, clustered at the start and irregular the rest of the way. That is the point of this page in one image. The shape was never the hard part.

Two pieces, and the split is deliberate.

The path is drawn in the driver from the session seed, so the same seed produces the same motion and a failure is reproducible. Every click, hover and drag arcs rather than teleporting, with the timing spread rather than uniform.

The event fields are set in the browser's own input handling, so pointerType, pressure and the trusted flag are what a real device produces rather than what a synthesiser guessed. That part is a source patch and it is the reason the feature is not simply a library you could add on top.

Getting the path itself right was not the end of it, either - the path also has to survive the driver's own hit-target check, which turned out to consume most of it on the exact calls real scripts make.

It is switched on by default and tunable:

with InvisiblePlaywright(seed=42, humanize=True) as browser:
    page = browser.new_page()
    page.goto("https://example.com")
    page.click("#submit")   # arcs to the button, with device-shaped events

Honest limit, since this page is about what things do not fix: humanised motion is a behavioural signal and it does nothing for the machine underneath. A convincing pointer on a browser announcing a software GPU is still a browser announcing a software GPU.

Checking your own

document.addEventListener('mousemove', e => {
  console.log(e.isTrusted, e.pointerType, e.pressure, e.movementX, e.timeStamp);
}, { once: false });

Drive your automation across the page with that attached and read the stream:

  • isTrusted must be true on every event. One false is decisive.
  • pointerType should say mouse consistently.
  • pressure should be 0 while hovering and non-zero between a down and an up.
  • The gaps between timestamps should vary. Identical gaps are a loop, not a hand.
  • Coordinates should not advance by a constant step.

Then do the same in a browser you drive by hand and compare the two streams. That comparison tells you more than any single field.

Short answers to the questions that lead here

Does moving the mouse along a Bezier curve avoid bot detection? It removes the teleporting-cursor tell, which is real. It does not change what the events say about where they came from.

Is page.mouse.move() detectable? The events it produces are trusted, so the cheapest check passes. What can still stand out is their timing and their fields.

Why is isTrusted false in my script? Because you dispatched the event from page JavaScript. That flag is set by the browser and cannot be assigned, which is why a JS-dispatched click can never be trusted while a driver-level one is.

Do I need a humanisation library? If your tool teleports the pointer, yes, or something equivalent. If it already draws paths, the next thing worth checking is the event fields rather than a better curve.

Does any of this matter if the site does not watch behaviour? No. Check whether the block arrives at the first request or only after an interaction. Late blocks point here; immediate ones point at the fingerprint or the address.

Should the movement be random every run? No. Random per run means a failing run cannot be reproduced. Derive it from a seed so the motion is varied between identities and identical when you replay one.

Sources

  • MDN: Event.isTrusted, for how the trusted flag is set by the browser and cannot be assigned from page script.
  • MDN: PointerEvent.pointerType, for the field a naive synthetic event usually gets wrong.
  • Playwright's documented Mouse.move(), the driver-level call every Bezier-curve humanizer plugs into.
  • This project's own source patch to the browser's input handling, and its own detection gates comparing the resulting event fields against a real device.

See also: the checklist for being detected on one site, where behaviour is step five, and three ways to make Playwright undetected, which is the same layering argument applied to the fingerprint instead of the pointer.


From the notes of invisible_playwright, a Firefox patched at the C++ level. The event fields are set in the engine, which is why they are not a library.