Detecting the OS, Browser and Browser Version before old methods Deprecate 💀

Julien Etienne
4 min readFeb 3, 2024

--

This stuff used to be simple, but for some annoying reason browser vendors keep switching how we access the platform’s data.

navigator.platform is deprecating for all browsers.

navigator.userAgent and navigator.appVersion are deprecating for Chrome.

Chromium browsers introduced: navigator.userAgentData .

But of course OF COURSE Mozilla and Safari don’t support this.

You know what your problem is?

For Safari and Firefox we can use the navigator.userAgent with RegExp to determine what it does have. Of course this can be spoofed, everything can be spoofed but that’s not our concern.

There’s a good example on StackOverflow which I cleaned up a little that does this in a comprehensive way (Don’t bother with AI for this stuff, it’s unlikely to understand the full picture anytime soon)

const ua = navigator.userAgent.toLowerCase().replace(/^mozilla\/\d\.\d\W/, '')

const mobiles = {
'iphone': /iphone/,
'ipad': /ipad|macintosh/,
'android': /android/
}

const desktops = {
'windows': /win/,
'mac': /macintosh/,
'linux': /linux/
}

const detectPlatform = () => {
// Determine the operating system
const mobileOS = Object.keys(mobiles).find(os => mobiles[os].test(ua) && navigator.maxTouchPoints >= 1)
const desktopOS = Object.keys(desktops).find(os => desktops[os].test(ua))
const os = mobileOS || desktopOS
// Extract browser information
const browserTest = ua.match(/(\w+)\/(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)/g)
const browserOffset = browserTest && (browserTest.length > 2 && !(/^(ver|cri|gec)/.test(browserTest[1])) ? 1 : 0)
const browserResult = browserTest && browserTest[browserTest.length - 1 - (browserOffset || 0)].split('/')
const browser = browserResult && browserResult[0]
const version = browserResult && browserResult[1]
return { os, browser, version }
}

This is not good enough because chrome is going to deprecate navigator.userAgent in favour of navigator.userAgentData let’s work that one out:

const detectPlatform = () => {
const userAgentData = navigator.userAgentData
const os = userAgentData.platform
const browser = userAgentData.brands[0]?.brand || ''
const version = userAgentData.brands[0]?.version || ''
return { os, browser, version }
}

Above we are getting the first object key brand. So now we know how to obtain both let’s merge. But I can see that the return for userAentData.brands is not reliable e.g. Chrome’s brand is index 1 but Opera’s is 2. I’ve fixed and normalised this in the final solution:

The solution

const navigatorErrorMessage = 'Could not find `userAgent` or `userAgentData` window.navigator properties to set `os`, `browser` and `version`'
const removeExcessMozillaAndVersion = /^mozilla\/\d\.\d\W/
const browserPattern = /(\w+)\/(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)/g
const engineAndVersionPattern = /^(ver|cri|gec)/
const brandList = ['chrome', 'opera', 'safari', 'edge', 'firefox']
const unknown = 'Unknown'
const empty = ''
const { isArray } = Array
let userAgentData = window.navigator.userAgentData
let userAgent = window.navigator.userAgent

const mobiles = {
iphone: /iphone/,
ipad: /ipad|macintosh/,
android: /android/
}

const desktops = {
windows: /win/,
mac: /macintosh/,
linux: /linux/
}

const detectPlatform = (customUserAgent, customUserAgentData) => {
// Use a provided UA string instead of the browser's UA
userAgent = typeof customUserAgent === 'string' ? customUserAgent : userAgent

// Use a provided UA data string instead of the browser's UA data
userAgentData = typeof customUserAgentData === 'string' ? customUserAgentData : userAgentData

if (userAgent) {
const ua = userAgent.toLowerCase().replace(removeExcessMozillaAndVersion, empty)

// Determine the operating system.
const mobileOS = Object.keys(mobiles).find(os => mobiles[os].test(ua) && window.navigator.maxTouchPoints >= 1)
const desktopOS = Object.keys(desktops).find(os => desktops[os].test(ua))
const os = mobileOS || desktopOS

// Extract browser and version information.
const browserTest = ua.match(browserPattern)
const versionRegex = /version\/(\d+(\.\d+)*)/
const safariVersion = ua.match(versionRegex)
const saVesion = isArray(safariVersion) ? safariVersion[1] : null
const browserOffset = browserTest && (browserTest.length > 2 && !(engineAndVersionPattern.test(browserTest[1])) ? 1 : 0)
const browserResult = browserTest && browserTest[browserTest.length - 1 - (browserOffset || 0)].split('/')
const browser = browserResult && browserResult[0]
const version = saVesion ? saVesion : browserResult && browserResult[1]

return { os, browser, version }
} else if (userAgentData) {
const os = userAgentData.platform.toLowerCase()
let platformData

// Extract platform brand and version information.
for (const agentBrand of userAgentData.brands) {
const agentBrandEntry = agentBrand.brand.toLowerCase()
const foundBrand = brandList.find(brand => { //eslint-disable-line
if (agentBrandEntry.includes(brand)) {
return brand
}
})
if (foundBrand) {
platformData = { browser: foundBrand, version: agentBrand.version }
break
}
}
const brandVersionData = platformData || { browser: unknown, version: unknown }
return { os, ...brandVersionData }
} else {
// Log error message if there's a problem.
console
.error(navigatorErrorMessage)

return {
// Ignore the VSCode strikethough. Disable linting line if necessary. This is just a fallback
os: navigator.platform || unknown,
browser: unknown,
version: unknown
}
}
}

export default detectPlatform

(This was a gist but slowly evolved into another repo 😱 . Please add an issue if you find any problems)

This should be suitable for mobile, tablet, desktop and such. For legacy platforms e.g. older game consoles e.g. PS4, HbbTV and such you can stick to the legacy methods for detecting.

“Is this it? That’s what it’s all about, Google?”

How I look at these vendors for complicating all this shit

No disrespect to Pfeiffer (The vendor), but that face on Pacino is an exact embodiment of how I feel knowing vendors make this shit harder every few years, like WTF.

— Julien

--

--

Julien Etienne
Julien Etienne

Written by Julien Etienne

Software Engineer Consultant (All articles are free) maximumcallstack.com

No responses yet