Class: Skadi::UserAgent
- Inherits:
-
Object
- Object
- Skadi::UserAgent
- Defined in:
- lib/skadi/user_agent.rb
Overview
A minimal user agent parser, designed for speed rather than completeness, aiming to detect the most common browsers and operating systems.
Why implement a custom user agent parsing script over an existing library?
Firstly, to eliminate the transient dependencies consumers of this gem are exposed to.
Secondly, existing libraries have relatively poor performance. Unlike other solutions, which use a large number of
regular expressions to detect browsers, this parser tokenises the user agent and uses a lookup table to identify the
majority of browsers with O(1) performance, using regular expressions as fallback. See the user_agent_test.rb file
for performance statistics.
Thirdly, the user agent parsing gems are years out of date. Modern browsers and modern bots are not properly detected, potentially skewing the analytics data collected.
Constant Summary collapse
- BROWSER_MATCHERS =
[ { regex: /Chrome\/(?<version>\d++).*WebView|; wv.*Chrome\/(?<version>\d++)/, browser: "Chrome WebView", }, { regex: /Android.*Version\/(?<version>\d++)/, browser: "Android Browser", os: "Android", }, { regex: /Android.*Chrome\/(?<version>\d++)/, browser: "Chrome for Android", os: "Android", }, { regex: /(?:iOS|iPod|iPad|iPhone).+(?:CriOS|Chrome)\/(?<version>\d++)/, browser: "Chrome for iOS", os: "iOS", }, { regex: /(?:CriOS|Chrome)\/(?<version>\d++)/, browser: "Chrome", }, { regex: /(?:iOS|iPod|iPad|iPhone).+Version\/(?<version>\d++)/, browser: "Safari for iOS", os: "iOS", }, { regex: /GSA\/(?<version>\d++)/, browser: "GSA", }, { regex: /(?:iOS|iPod|iPad|iPhone).*Safari/, browser: "Safari for iOS", os: "iOS", }, { regex: /Version\/(?<version>\d++).*Safari/, browser: "Safari", }, { regex: /Safari\//, browser: "Safari", }, { regex: /WebKit\/(?<version>\d++)/, browser: "WebKit", }, { regex: /Mozilla\/(?<version>\d++).*rv:(?<engine_version>\d++).*?Gecko\//, browser: "Mozilla", engine: "Gecko", }, ]
- BROWSER_TOKENS =
{ "AlohaBrowser" => { browser: "Aloha", }, "Avast" => { browser: "Avast Secure Browser", }, "AVG" => { browser: "AVG Secure Browser", }, "baiduboxapp" => { browser: "Baidu", }, "BingSapphire" => { browser: "Bing", }, "Brave" => {}, "Chromium" => { regex: /Chromium[\/ ](?<version>GOST|\d++)/, }, "Ddg" => { browser: "DuckDuckGo", }, "DuckDuckGo" => {}, "Ecosia" => { regex: /Ecosia ios@(?<version>\d++)/, os: "iOS", }, "Edg" => [ { regex: /(?:iOS|iPod|iPad|iPhone).*Edg\/(?<version>\d++)/, browser: "Edge for iOS", os: "iOS", }, { regex: /Android.*Edg\/(?<version>\d++)/, browser: "Edge for Android", os: "Android", }, { regex: /Edg\/(?<version>\d++)/, browser: "Edge", }, ], "EdgA" => { browser: "Edge for Android", os: "Android", }, "Edge" => {}, "EdgiOS" => { browser: "Edge for iOS", os: "iOS", }, "Electron" => {}, "FBAV" => { browser: "Facebook", }, "Firefox" => [ { regex: /(?<browser>PaleMoon|Waterfox)\/(?<version>\d++)/, }, { regex: /(?:iOS|iPod|iPad|iPhone).*Firefox\/(?<version>\d++)/, browser: "Firefox for iOS", os: "iOS", }, { regex: /Android.*Firefox\/(?<version>\d++)/, browser: "Firefox for Android", os: "Android", }, { regex: /Firefox\/(?<version>\d++)/, browser: "Firefox", }, ], "FxiOS" => { browser: "Firefox for iOS", }, "HeadlessChrome" => { browser: "Chrome Headless", }, "HeyTapBrowser" => { browser: "HeyTap", }, "HuaweiBrowser" => { browser: "Huawei Browser", }, "Instagram" => { regex: /Instagram[\/ ](?<version>\d++)/, }, "Iron" => { regex: /Chrome\/(?<version>\d++).*?Iron/, }, "KAKAOTALK" => { regex: /KAKAOTALK[\/ ](?<version>\d++)/, }, "Konqueror" => {}, "Line" => {}, "LinkedInApp" => { regex: /\[LinkedInApp\]\/(?<version>\d++)/, browser: "LinkedIn", }, "Maxthon" => {}, "MicroMessenger" => { browser: "WeChat", }, "MiuiBrowser" => { browser: "MIUI Browser", }, "MQQBrowser" => {}, "MSIE" => { regex: /MSIE (?<version>\d++)(?>.*(?<engine>Trident)\/(?<engine_version>\d++))?|(?<engine>Trident)\/(?<engine_version>\d++).*rv[: ](?<version>\d++)/, browser: "IE", os: "Windows", }, "musical" => { regex: /musical_ly_(?<version>\d++)/, browser: "TikTok", }, "Norton" => { browser: "Norton Private Browser", }, "OculusBrowser" => { browser: "Oculus Browser", }, "Opera" => [ { regex: /Opera Mini[\/ ](?<version>\d++)/, browser: "Opera Mini", }, { regex: /Opera(?>.*Version)?[\/ ](?<version>\d++)/, browser: "Opera", }, ], "OPR" => { browser: "Opera", }, "OPT" => { browser: "Opera Touch", }, "OPX" => { browser: "Opera GX", }, "PaleMoon" => {}, "QQBrowser" => {}, "QuarkPC" => { browser: "Quark", }, "SamsungBrowser" => { browser: "Samsung Internet", }, "SeaMonkey" => {}, "Silk" => {}, "Snapchat" => {}, "TikTokLIVEStudio" => {}, "Trident" => { regex: /MSIE (?<version>\d++).*Trident\/(?<engine_version>\d++)|Trident\/(?<engine_version>\d++).*rv[: ](?<version>\d++)/, browser: "IE", engine: "Trident", os: "Windows", }, "Twitter" => { regex: /Twitter for iPhone\/(?<version>\d++)/, }, "UCBrowser" => {}, "VivoBrowser" => { browser: "Vivo Browser", }, "Whale" => {}, "YaBrowser" => { browser: "Yandex", }, "YaSearchBrowser" => { browser: "Yandex", }, }
- ENGINE_MATCHERS =
[ { regex: /AppleWebKit\/537\.36.*Edge\/(?<version>1[2-8])\./, engine: "EdgeHTML", }, { regex: /AppleWebKit\/537\.36.*Chrome\/(?<version>\d++)/, engine: "Blink", }, { regex: /(?<engine>WebKit|Presto|Trident|Goanna)\/(?<version>\d++)/, }, { regex: /rv:(?<version>\d++).*?Gecko\/\d++/, engine: "Gecko", }, ]
- OS_TOKENS =
{ "CFNetwork" => "iOS", "CrOS" => "Chrome OS", "Fedora" => "Fedora", "Gentoo" => "Gentoo", "HarmonyOS" => "HarmonyOS", "iPad" => "iOS", "iPhone" => "iOS", "iPod" => "iOS", "Mac" => "macOS", "Ubuntu" => "Ubuntu", "Windows" => "Windows", }
- BOT_GLOBAL_MATCHERS =
%w[bot crawl scan spider]
- BOT_WORD_SET =
Set.new(%w[AGENT Agent AppInsights ArchiveBox Archiver Archiving BingPreview BrandVerity Butterfly Charlotte Checkly Claude CloudFlare Cloudflare Code Collapsify CookieHubVerify Criticalcss Daily DareBoost DatadogSynthetics Datanyze Devin Dlc FeedBurner Feeder Feedly FlipboardProxy Fluid Foregenix GTmetrix GeedoProductSearch GeedoShopProductFinder Google GoogleAgent GoogleImageProxy GotSiteMonitor Hardenize HeadlessChrome Hotjar Inspector Lighthouse LinkTiger Mail Manus MarketGoo MarketingMiner MetaIAB Miniature MonitoRSS Monitor Netcraft NewRelicSynthetics NewsBlur NewsNow Newsify Nitro OpenGraph Optimizer PTST PWABuilderHttpAgent Perplexity PingdomTMS Playwright Preview PrintFriendly Puppeteer Readable RevvimGort Rigor SQWatcher Scope3 SecurityHeaders Selenium SeoSiteCheckup Silktide Sindup Siteimprove Specificfeeds Sucuri TestLocally ThousandEyes Trae YLT ZoteroTranslationServer adbeat agent archiver archiving brandverity butterfly claude cloudflare code contentkingapp deadlinkchecker devin europarchive feeder feedly google img2dataset infegy mail mailservertest2023 marketingminer mirrorweb monitor nbertaupete95 netcraft newsai newsblur newsify opencode opengraph oupwis perplexity preview retrevo scope3 scraping seositecheckup sitebulb slider splash sqwatcher turingos ubermetrics uptimedoctor watchTowr webresearch websitepulse woorankreview xmco])
- BOT_FALLBACK_MATCHERS =
[ "AP3A.240617.008" ]
Instance Method Summary collapse
- #bot? ⇒ Boolean
- #browser ⇒ Object
- #browser_version ⇒ Object
- #engine ⇒ Object
- #engine_version ⇒ Object
- #human? ⇒ Boolean
-
#initialize(user_agent) ⇒ UserAgent
constructor
A new instance of UserAgent.
- #os ⇒ Object
- #to_h ⇒ Object
Constructor Details
#initialize(user_agent) ⇒ UserAgent
Returns a new instance of UserAgent.
17 18 19 |
# File 'lib/skadi/user_agent.rb', line 17 def initialize(user_agent) @user_agent = (user_agent || "")[0, 2048] end |
Instance Method Details
#bot? ⇒ Boolean
51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/skadi/user_agent.rb', line 51 def bot? return @bot unless @bot.nil? # If the user agent doesn't follow the normal browser patterns, we assume it's a bot parse_browser if @browser.nil? if @browser == "Unknown" @bot = true return true end @bot = detect_bot end |
#browser ⇒ Object
21 22 23 24 25 |
# File 'lib/skadi/user_agent.rb', line 21 def browser parse_browser if @browser.nil? @browser end |
#browser_version ⇒ Object
27 28 29 30 31 |
# File 'lib/skadi/user_agent.rb', line 27 def browser_version parse_browser if @browser_version.nil? @browser_version end |
#engine ⇒ Object
33 34 35 36 37 |
# File 'lib/skadi/user_agent.rb', line 33 def engine parse_engine if @engine.nil? @engine end |
#engine_version ⇒ Object
39 40 41 42 43 |
# File 'lib/skadi/user_agent.rb', line 39 def engine_version parse_engine if @engine_version.nil? @engine_version end |
#human? ⇒ Boolean
65 |
# File 'lib/skadi/user_agent.rb', line 65 def human? = !bot? |
#os ⇒ Object
45 46 47 48 49 |
# File 'lib/skadi/user_agent.rb', line 45 def os parse_os if @os.nil? @os end |
#to_h ⇒ Object
67 68 69 70 71 72 73 74 75 76 |
# File 'lib/skadi/user_agent.rb', line 67 def to_h { browser: browser, browser_version: browser_version, engine: engine, engine_version: engine_version, os: os, bot: bot?, } end |