Generates realistic browser sessions on any website. Configured via config.json.
Each simulated visit:
- Picks a random browser/device profile with weighted distribution (configurable)
- Lands on the first page in
pageswithfirstPagePriority% probability, otherwise a random page from the pool - Follows a persona (quick scanner, engaged reader, form filler, window shopper, power user) that controls scroll depth, click rate, read time, and number of pages visited
- Simulates human mouse movement along bezier curves, smooth scrolling, text selection, form interaction, and occasional rage clicks / tab switches
- Spends at least
pageMinMsms per page before navigating away - Sends a realistic
Refererheader (Google, Bing, social, or direct) - Optionally injects a custom script at the start of every page load
| Profile | Engine | UA | Default weight |
|---|---|---|---|
desktopChrome |
Firefox | Chrome/136 Windows/Mac | 40 |
iphone14 |
WebKit | Mobile Safari | 20 |
pixel7 |
Firefox | Chrome/136 Android | 12 |
desktopFirefox |
Firefox | Firefox | 10 |
desktopSafari |
WebKit | Safari | 10 |
ipadPro |
WebKit | iPad Safari | 8 |
Chrome sessions run on the Firefox engine to avoid Playwright's Chromium headless-shell being fingerprinted by bot filters.
All settings live in config.json:
{
"targetUrl": "https://example.com",
"pages": ["/", "/about", "/pricing"],
"visitCount": 1000,
"concurrency": 5,
"firstPagePriority": 60,
"sessionTimeoutMs": 90000,
"pageMinMs": 4000,
"snippet": null,
"devices": { "desktopChrome": 40, "iphone14": 20, "pixel7": 12, "desktopFirefox": 10, "desktopSafari": 10, "ipadPro": 8 },
"personas": [...]
}| Field | Default | Description |
|---|---|---|
targetUrl |
— | Root URL of the site (required; trailing slash stripped automatically) |
pages |
["/"] |
Pages to visit. If empty or omitted, only the root is visited. |
visitCount |
1000 |
Total sessions to run |
concurrency |
5 |
Parallel browser contexts (also overridable via CONCURRENCY env var) |
firstPagePriority |
60 |
% of sessions that land on the first entry in pages |
sessionTimeoutMs |
90000 |
Hard timeout per session in ms |
pageMinMs |
4000 |
Minimum dwell time per page in ms |
snippet |
null |
JS injected at the start of every page load (see below) |
devices is a map of profile name → relative weight. Omit a key to disable that profile; set a weight to 0 to disable it. Weights don't need to sum to 100.
"devices": {
"desktopChrome": 40,
"iphone14": 20,
"pixel7": 12,
"desktopFirefox": 10,
"desktopSafari": 10,
"ipadPro": 8
}personas is an array of visitor behavior profiles. Each session picks one at random. All fields have defaults so you only need to specify what you want to override.
| Field | Default | Description |
|---|---|---|
name |
"custom" |
Label shown in logs |
scrollDepth |
0.5 |
Fraction of page height scrolled (0–1) |
clickRate |
0.5 |
Probability of clicking a link or element (0–1) |
readTime |
[1000, 3000] |
[min, max] ms spent reading per page |
pageCount |
[1, 3] |
[min, max] pages visited per session |
idleChance |
0.08 |
Probability of pausing mid-session (0–1) |
resizeChance |
0.1 |
Probability of resizing the window (0–1) |
uTurnChance |
0.1 |
Probability of hitting back before finishing a page (0–1) |
"personas": [
{ "name": "quick_scanner", "scrollDepth": 0.3, "clickRate": 0.5, "readTime": [500, 1800], "pageCount": [1,2], "idleChance": 0.04, "resizeChance": 0.0, "uTurnChance": 0.2 },
{ "name": "engaged_reader", "scrollDepth": 0.9, "clickRate": 0.7, "readTime": [2000, 5000], "pageCount": [2,4], "idleChance": 0.15, "resizeChance": 0.1, "uTurnChance": 0.05 },
{ "name": "form_filler", "scrollDepth": 0.6, "clickRate": 0.9, "readTime": [1000, 3000], "pageCount": [2,3], "idleChance": 0.10, "resizeChance": 0.05, "uTurnChance": 0.05 },
{ "name": "window_shopper", "scrollDepth": 0.5, "clickRate": 0.6, "readTime": [800, 2500], "pageCount": [2,5], "idleChance": 0.08, "resizeChance": 0.15, "uTurnChance": 0.3 },
{ "name": "power_user", "scrollDepth": 1.0, "clickRate": 0.9, "readTime": [1500, 4000], "pageCount": [3,6], "idleChance": 0.12, "resizeChance": 0.2, "uTurnChance": 0.1 }
]Set snippet to inject any JavaScript at the start of every page load — analytics, session replay, heatmap tools, etc. Accepts either a raw JS string or a full <script>…</script> block; the tags are stripped automatically.
{
"snippet": "<script>\nconsole.log('injected');\n</script>"
}Pass a config file path as the first argument:
node generate_visits.js /path/to/other-site.jsondocker compose up --buildOverride concurrency without editing the config:
CONCURRENCY=10 docker compose up --buildnpm install
npx playwright install firefox webkit
node generate_visits.js