User Agent Parser for Ruby
Paste the user agent string from a Rails log, a support ticket or a Rack request, read the browser, engine, platform and device it names, and take away the Ruby that reads the same header in a controller. The parsing happens in your browser.
The string to parse
What the string claims
- Browser
- Chrome 126.0.0.0
- Engine
- Blink
- Operating system
- macOS 10.15.7
- Device
- Desktop
The same header in a Rails controller
A concern that reads request.user_agent
module ClientDescription
extend ActiveSupport::Concern
private
def client
raw = request.user_agent.to_s
return {} if raw.blank?
agent = UserAgent.parse(raw)
{
browser: [agent.browser, agent.version.to_s.presence].compact.join(" "),
operating_system: agent.os.presence || agent.platform,
device: agent.mobile? ? :mobile : :desktop
}
end
endThe gem reports browser, version, platform and os, so there is no engine row to fill - the string names a frozen AppleWebKit version whatever engine is really rendering. Add the browser gem when you need the engine, bot detection or device predicates.
What this browser reports
This browser's user agent
- Browser
- —
- Engine
- —
- Operating system
- —
- Device
- —
- Screen
- —
- Viewport
- —
- Pixel ratio
- —
- Colour scheme
- —
- Timezone
- —
- Language
- —
- Cookies
- —
- Storage
- —
Everything runs in your browser. Nothing is uploaded.
What the parts of the string actually mean
Every string on this page opens with Mozilla/5.0, and none of the browsers sending it are Mozilla. That prefix is a fossil: servers once checked for it before serving frames, so everybody claimed it, and it has meant nothing since. What follows is a bracketed comment describing the machine - the platform, sometimes an architecture, on a phone often a device model - and then a run of product tokens, each a name and a version separated by a slash. The tokens are where the negotiation shows. Chrome on a desktop sends AppleWebKit/537.36, then Chrome/126.0.0.0, then Safari/537.36, naming two engines and a competitor, because pages written for Safari checked for Safari and pages written for WebKit checked for WebKit. Firefox carries Gecko in the same spirit. Read the tokens right to left and you are reading a history of what servers used to refuse, which is why the last token is almost never the browser that sent the request.
Reading the same header in Rails
In a controller the whole string is request.user_agent, and the first thing worth knowing is that it can be nil - the header is optional, and a health check or a bare Net::HTTP call will not send it. Coerce with to_s and guard on presence before parsing, because UserAgent.parse of an empty string returns a placeholder rather than nothing. The parser itself is probably already installed: useragent comes in as an actionpack dependency, which is why the snippet above needs no new gem, though code of your own that calls it should name it in the Gemfile rather than rely on somebody else's. It reports browser, version, platform and os, and stops there - no rendering engine, and no device type beyond mobile?. When you need bot detection or real device predicates, the browser gem is the one to add. Wherever the logic lands, put it in a concern and return a hash or a small object, so the view asks for a device and never sees the header.
Why the string keeps shrinking
The string is getting shorter on purpose, and code that depends on its detail is on a clock. Chrome's user agent reduction froze the minor parts of the version to 0.0.0 and cut the platform down to a coarse value, so a machine reporting macOS 10.15.7 may be running something years newer - the number is a compatibility floor now, not a fact. The replacement is Sec-CH-UA and its siblings, client hints the browser sends as separate headers and only expands when a site asks for the high-entropy parts, which makes the detail opt-in and auditable rather than broadcast to everybody. For most decisions neither is the right input: ask whether the feature exists rather than who the browser claims to be, and let CSS answer layout. And remember what a branch on this header costs even when it is justified - two responses at one URL means Vary: User-Agent, which fragments every cache between you and the reader, at a cache-key granularity that is effectively per-browser-build.