The part between the quotes is written by the client itself
I was going through an Nginx access.log one time, trying to confirm who kept hitting an API: a search engine spider, a real person inside WeChat's in-app browser, or someone with a script pretending to be Chrome. The only evidence is the User-Agent between the quotes, and the client assembles it and sends it verbatim. "Looks like Chrome" and "is Chrome" are two different propositions: the first is about the literal string, the second needs something beyond it.
Set the scale first: across 8 samples the longest is 274 characters and the shortest is curl/8.5.0 at 10 characters, 935 in total. How many segments there are, and in what order, is decided entirely by the sending end.
Figure 1 | Character counts for the 8 samples, with bars scaled by character count.
What each of the six dimensions reads, and which table the value comes from
| Dimension | Where the value comes from | Shown when unavailable |
|---|---|---|
| Browser / client | BROWSERS, 32 entries, first match from top to bottom | Unrecognized |
| Rendering engine | A separate chain: Trident, Edge, bare Chromium substring, Gecko | — (not a browser UA) |
| Operating system | Windows NT mapping table, 7 hits | — (cannot determine) |
| Device type | iPad, iPhone, whether Mobile is present, then check whether it is a crawler | — (cannot determine) |
| CPU architecture | Intel Mac first, arm64 versus x64, WOW64 judged separately | — (not declared) |
| Locale | Of the 8 samples only Android WeChat reports zh-cn | The other 7 — (not declared) |
The Android WeChat line contains both AppleWebKit/537.36 and MicroMessenger/8.0.40.2420; the client column gives WeChat (in-app browser) 8.0.40.2420, while the engine column gives Blink (Chromium engine) / 537.36. Safari/Mac gives WebKit (Safari engine) / 605.1.15; each axis is judged on its own.
One string, and position decides who it becomes
- MicroMessenger is entry 1, so Chrome/77.0.3969.139 inside the Android WeChat line never gets a turn.
- Edg, entry 13, comes before legacy Edge at entry 14; Chrome is entry 28, Firefox entry 30 and Safari entry 31.
- The crawler table has 21 entries, and the catch-all sits after the named spiders and the libraries.
- Libraries go through the crawler table too: curl/8.5.0 is entry 13 and python-requests/2.31.0 entry 15.
Figure 2 | Match position of each client type among the 32 browser entries, with bars scaled by position.
The crawler side has higher priority: detectBot runs first, and if it returns anything the browser table is skipped. The forged compatible; Spybot/1.0 carries a legitimate Chrome/131.0.0.0, so it could have matched entry 28, yet the card says automated program / crawler, major version bot: it matches the last catch-all among the 21 entries, /(bot|spider|crawler|fetcher|httpclient)\b/i. With only compatible; GenericClient/1.0 the catch-all does not fire, it falls back to Chrome 131.0.0.0, and both the operating system and the device are — (cannot determine).
Why Blink shows up in the engine column by itself
The Chromium branch in detectEngine is the bare substring /Edg|OPR|Vivaldi|Chrome|CriOS|Chromium/, with no i modifier. Lowercase therefore does not count: 3 UAs containing only pledge, edgeless or knowledge-sync match it 0 times, and the last one still reports Gecko 132.0.
Uppercase gets caught: 4 custom UAs containing an uppercase substring are reclassified in exactly 4 cases, namely the model name OPRO-X, the device name Chromecast and EdgeSync-Agent/2.0, all reported as Blink (Chromium engine) with an empty version; one Firefox 128.0 turns from Gecko into Blink just because MicrosoftEdgeRuntime is appended, matching the real Edge control case at entry 5. An engine name with an empty version is the tell.
Who replaced those underscores in the version number
iOS captures 17_4 and shows iOS 17.4 after the dot replacement, while macOS 10_15_7 shows 10.15.7. The Windows mapping table has only 7 hits, from 10.0 for Windows 10 and 11 down to 5.2 for Windows Server 2003, and anything unmapped falls back to older version: NT 11.0, 6.4 and 5.0 all count as older, while real Windows 11 actually goes through 10.0. All 4 segments of WeChat 8.0.40.2420 are kept, (0x28002837) does not join the version, and rv:132.0 takes precedence over 20100101.
The third table explains the literal text only and draws no conclusions
The 34 TOKENS entries only explain what each segment means. Highlighting the 125-character Edge/Win line hits 9 fragments, while 25 entries either do not match or are removed by interval overlap. Two comments answer the opening question: Mozilla/5.0 is written as 5.0 by almost every browser and does not indicate the browser brand, and (KHTML, like Gecko) is a WebKit masquerade declaration. I cannot memorize those 34 entries, so I usually drop the whole line into the User-Agent Parser and have each segment labeled.
The "who exactly is it" column cannot answer that. Telling a spider from a pretender requires taking the IP from the same log line and checking its location and reverse DNS; only the data-center clue from the IP location lookup can answer whether this machine could be a Baidu egress. Sec-CH and Client Hints each appear 0 times in the template, so a reduced UA frozen at 108.0.0.0 can only read as Chrome 108.0.0.0; when an encoded segment hangs off the end, decode it with the Base64 encoder and decoder first.
Q: Can a UA be forged?
Yes, and it takes no skill: the client assembles that string in the first place. With a crawler keyword it lands on automated program / crawler; without one it falls back to Chrome 131.0.0.0. It reads the literal text, not the origin, so any access control built on this column alone can be bypassed.
Q: Why does Chrome's UA say Safari?
Compatibility legacy. Chromium-based UAs all end with a Safari/537.36 segment, and the comment says Chromium appends one at the end as a compatibility leftover. The engine column ignores names: Chrome, Edge and Android WeChat all report Blink (Chromium engine) / 537.36.
Q: Is detecting a crawler the same as blocking one?
No. The 21-entry crawler table only answers whether the string looks like a crawler; entry 21 is the generalized match on bot, spider, crawler, fetcher and httpclient. The decision depends on IP and reverse DNS. A script that calls itself Googlebot is still recognized as a spider.
Q: Why does the WeChat version in the page description differ from the card?
The code captures the 4 segments of 8.0.40.2420, so the card shows that; the MicroMessenger/8.0.40 in the tool description is one segment short. The same description also mentions mini programs, while miniProgram appears 0 times in the template, so that UA is simply read as WeChat (in-app browser) 8.0.44.