DEV Community

Vladimir Elchinov for Session Replay

Posted on

Your Bug Report Says Intel Mac on Catalina. The Machine Is Neither.

Here is the User-Agent string Chrome sent from the Mac I am writing this on, read out of the browser a few minutes ago:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Enter fullscreen mode Exit fullscreen mode

Three of those facts are false. The machine is not Intel, it is ARM. The OS is not 10.15.7, which is Catalina and shipped in 2020; it is 26.6.2. And Chrome is not 154.0.0.0, it is 154.0.8037.93.

None of that is a bug. It is the documented design. Chromium's own User-Agent reduction page says the macOS platform version is frozen at 10_15_7 and the browser version is reduced to 0.0.0, with the reduction fully in place from Chrome 113. The macOS freeze exists because when browsers started reporting Mac OS X 11_*, a long tail of sites broke on it.

So the string is behaving correctly. The problem is what we do with it.

A measured field can be confidently wrong

Most advice about environment fields in bug reports is about getting them measured instead of typed, because a person recalling their OS version is guessing and a script reading it is not. That advice is right and it is not enough.

If your collector reads navigator.userAgent and parses it, your tickets now carry an OS field that is measured, consistent, and wrong on every Mac you will ever receive a report from. It is worse than a typed field in one specific way: nobody audits it. A typed macOS, I think the latest one invites a follow-up question. Intel Mac OS X 10.15.7, sitting in the field a script filled in, reads as fact.

Then somebody tries to reproduce. The ticket says Intel, Catalina. If you are diligent, you go and find an Intel machine, or spin up a VM with an OS from 2020, and you fail to reproduce a rendering bug that only happens on Apple silicon. You write cannot reproduce and close it. The reporter, who is on an ARM Mac on macOS 26, files it again next week.

That is the cost, and it is paid by whoever is least able to argue with the field.

Getting the real values

Everything the string used to carry is still available, through client hints. The catch is the shape of the API.

const d = navigator.userAgentData;

const hints = d?.getHighEntropyValues
  ? await d.getHighEntropyValues([
      "platformVersion",
      "architecture",
      "bitness",
      "uaFullVersion",
    ])
  : null;
Enter fullscreen mode Exit fullscreen mode

On the machine above that returns:

{
  "platform": "macOS",
  "platformVersion": "26.6.2",
  "architecture": "arm",
  "bitness": "64",
  "uaFullVersion": "154.0.8037.93",
  "model": "",
  "mobile": false
}
Enter fullscreen mode Exit fullscreen mode

Which is the truth, and not a value you can get any other way.

Two things about that snippet are load-bearing.

It is a promise. getHighEntropyValues is asynchronous, and a collector that builds its payload synchronously when somebody clicks a button will post before it resolves. The failure mode is not an error: the field is simply absent or empty, on every report, and nobody notices for months because an empty environment field looks like a configuration choice. If your capture runs in a click handler, the await has to be inside it and the submit has to wait.

It is not everywhere. MDN marks navigator.userAgentData as experimental, with "limited availability" because it "does not work in some of the most widely-used browsers". So the optional chaining is not decoration. Where the API is missing you are back to the frozen string, and the honest thing is to record that you could not measure the OS version rather than to parse 10_15_7 and present it as one.

That second branch is the one worth building deliberately. A field that says unknown is a field somebody will ask about. A field that says Catalina sends them to the wrong machine.

The general shape

The useful habit is not "measure, don't ask". It is: know what each field is measuring, and what it is capable of measuring.

The UA string is measuring what the browser has decided to disclose by default, which since 2023 is deliberately less than the truth. Client hints measure the truth, if you ask for it, asynchronously, where the API exists. Those are two different instruments with the same name on the form, and only one of them can answer the question your reproduction attempt is about to depend on.

This one came out of a comment from @phongdesigns on an earlier piece here, who pointed out that measured is not the end of the question and measured by what is. That is the sharper version, and it is the one I would start from now.

Top comments (0)