Skip to content
← 1978.io blog.1978.io

Scry: is that visitor even a browser?

· 3 min read

Here's a real User-Agent string, the thing every browser sends with every request:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0

That's Microsoft Edge. It claims to be Mozilla, says it's "like Gecko", mentions Safari and name-drops Chrome along the way. The only honest part is the Edg at the very end. Every browser does this, for reasons that go back to the 90s and involve a lot of websites checking for the wrong thing. The result is a string that's technically readable and practically useless.

I wanted my apps' login logs to say "Chrome on Windows" instead of that, and to notice when a "visitor" was actually curl or a script. That's all.

The obvious answer, and why I didn't take it

The well-known tool for this is matomo/device-detector, and it's excellent. It can tell you the exact browser version, the phone's make and model, and which of hundreds of crawlers just hit you. To do that it carries a big, constantly maintained database of patterns, because the world of User-Agents never stops changing.

For what I needed, that's a lot of library. I didn't want versions or phone models. I wanted four answers.

Four questions, and no more

Scry answers exactly these:

  1. Is this a real browser? Or is it curl, wget, a Python script, Postman, headless Chrome, a crawler?
  2. Which browser? Chrome, Firefox, Safari, Edge and friends.
  3. Which OS? Windows, macOS, iOS, Android, Linux and so on.
  4. Desktop, tablet or mobile?

And it's just as clear about what it won't do: no version numbers, no device models, no catalog of named bots. That's not laziness, it's the whole design. Every one of those is a permanent maintenance job, and saying no to them is what keeps Scry to one file you can read in a sitting. If you need that precision, the README sends you straight to device-detector.

use Scry\Scry;

$d = Scry::detect();          // reads the current request

if (!$d->isBrowser()) {
    // curl, a script, a crawler...
}

echo $d->label();             // "Chrome on Windows"

The bits I'm quietly proud of

  • It never guesses. Give it an empty or nonsense string and you get unknown with a confidence of low, never a confident wrong answer. And raw() always hands back the original string, so you never lose the truth to a label.
  • Client Hints first. Modern Chromium browsers send cleaner Sec-CH-UA headers alongside the User-Agent. Scry prefers those when they're there, and ignores the deliberately fake "GREASE" brand names browsers mix in to stop servers hard-coding things. (Yes, browsers lie on purpose now, to stop us relying on the lies they told before.)
  • The iPad problem. Since iPadOS 13, an iPad in Safari says it's a Mac. Scry's test corpus has that case in it, along with phones, tablets, bots, tools, empty strings and garbage.
  • No dev dependencies for the tests either. php tests/run.php and you're done.

About the name

It started life as "Sift", until a quick search found a well-known company already using it. Scry stuck: to scry is to read the future in a crystal ball, which felt about right for a library that squints at a string of text and tells you what's on the other end.

Try it

  • Composer: composer require 1978io/scry (Packagist)
  • Or just copy the file. src/Scry.php is the whole library. Drop it in and require it.
  • Live demo: bits.1978.io/scry shows what it makes of your own browser.
  • Code: github.com/1978io/scry. PHP 8.0+, MIT.

A JavaScript version for Node is on the someday list. If that would be useful to you, tell me and it moves up.

Share this post

← All posts