Guise

Session-sticky User-Agent rotation with modern, refreshable browser profiles.

Rotating a fresh random UA on every request is a tell — real clients keep one identity for a while. Guise assigns one realistic browser profile (User-Agent + matching Accept-Language) per session key and keeps it sticky for 30 minutes, so your traffic looks like a handful of real browsers rather than a random-UA firehose.

def deps do
[{:guise, "~> 0.1"}]
end

Usage

# Random, share-weighted UA string
Guise.random()
#=> "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/150.0.0.0 Safari/537.36"
# Headers for a one-off request
Guise.headers("https://example.com/")
#=> [{"user-agent", "…"}, {"referer", "https://example.com/"}]
# Sticky headers for a session — same key → same browser identity for 30 min
Guise.headers("https://example.com/", {user_id, "example.com"})
#=> [{"user-agent", "…"}, {"accept-language", "en-US,en;q=0.9"}, {"referer", "https://example.com/"}]

Guise starts its own supervised process (an ETS-backed session store), so there's nothing to add to your supervision tree.

The pool

A modern UA string is a stable template plus one volatile part — the browser's major version (Chrome froze everything below the major to .0.0.0 years ago). Guise renders its pool from templates (engine × OS × weight) against a small version table, so the whole pool modernizes when a handful of numbers do — and can't produce an inconsistent string.

The default pool spans desktop (Chrome/Firefox/Edge/Safari across Windows, macOS and Linux) and mobile (Chrome on Android, Safari on iPhone and iPad), share-weighted so Chrome-heavy rotation looks like real traffic.

Keeping it fresh

mix guise.refresh

Pulls the current stable Chrome and Firefox majors from endoflife.date and rewrites priv/versions.json. Edge is derived from Chrome (Chromium tracks it); Safari has no clean feed so it's maintained by hand. The runtime never hits the network — it only reads the committed file at boot. (guise.refresh uses OTP's built-in :httpc, so the package itself adds no HTTP dependency.)

Configuration

Pin or override individual versions:

config :guise, Guise,
versions: %{"chrome" => "150.0.0.0"}

Or replace the pool wholesale with your own profiles — each a map with a required :ua and optional :accept_language (default en-US) and :weight (default 1); a bare string is taken as the UA. When :profiles is set it wins over the built-ins and :versions is ignored:

config :guise, Guise,
profiles: [
%{ua: "MyBot/1.0 (+https://example.com/bot)", weight: 3},
"Mozilla/5.0 (…) Chrome/150.0.0.0 Safari/537.36"
]

License

MIT © Alexander Don