go-ua-parser

July 24, 2026 · View on GitHub

A high-performance, production-grade HTTP User-Agent parser for Go with first-class bot and AI crawler detection.

Go Reference Go Report Card CI License: MIT GitHub Issues Share on X Share on Reddit

Features

  • 1,666 detection rules: 385 browsers, 821 bots, 76 devices, 368 in-app browsers, 16 engines
  • 35 OS types: Windows (incl. Windows 11 via Client Hints), macOS, iOS, iPadOS, Android, ChromeOS, Linux distros (Ubuntu, Fedora, Debian, Arch, etc.), HarmonyOS, Tizen, KaiOS, FreeBSD, watchOS, tvOS, and more
  • First-class bot detection: 821 bots across 7 classes — search engines, social bots, AI crawlers (GPTBot, ClaudeBot, DeepSeekBot, Bytespider), SEO tools, monitoring, scrapers, security scanners
  • 14 heuristic patterns: Catches unnamed bots via keywords (bot, crawler, spider, scraper, fetcher, archiver, validator, monitoring, etc.)
  • Client Hints support: Sec-CH-UA-* headers for accurate modern browser detection, Windows 11 detection
  • Browser channel detection: nightly, beta, dev, canary from version patterns
  • High performance: ~500K+ parses/sec per core, zero-alloc cache hits at 32ns
  • Concurrency-safe: Immutable Parser, sharded LRU cache (16 shards, 82ns contended)
  • Framework-ready: Middleware examples for Gin, Echo, Chi, Fiber, Prometheus + ParseRequest(*http.Request)
  • Observability: OpenTelemetry attribute helper (contrib/otel), Prometheus example, pre/post-parse hooks
  • Extensible: Custom rules, pre/post-parse hooks
  • Pure Go: Zero external runtime dependencies

Quick Start

package main

import (
    "fmt"
    uax "github.com/motiv8-team/go-ua-parser"
)

func main() {
    r := uax.Parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/123.0.6312.86 Safari/537.36")

    fmt.Println(r.Browser.Name)    // "Chrome"
    fmt.Println(r.Browser.Version) // "123.0.6312.86"
    fmt.Println(r.Browser.Major)   // "123"
    fmt.Println(r.Browser.Family)  // "Chromium"
    fmt.Println(r.Engine.Name)     // "Blink"
    fmt.Println(r.OS.Name)         // "Windows"
    fmt.Println(r.CPU.Architecture)// "x86_64"
    fmt.Println(r.DeviceClass())   // "desktop"
    fmt.Println(r.IsBot)           // false
    fmt.Println(r.IsMobile)        // false
}

Installation

go get github.com/motiv8-team/go-ua-parser

Requires Go 1.22+. Zero external dependencies.

API

Parse Functions

// Global one-liner (uses sync.Once default parser)
r := uax.Parse(ua)

// Reusable parser (recommended for servers)
parser, err := uax.NewParser()
r := parser.ParseString(ua)

// With Client Hints
r := parser.Parse(uax.Input{UAString: ua, ClientHints: ch})

// From *http.Request (auto-extracts UA + all Client Hints headers)
r := parser.ParseRequest(httpReq)

// Zero-alloc variants (reuse caller-owned Result)
parser.ParseInto(input, &result)
parser.ParseRequestInto(httpReq, &result)

// Bot-only fast path
bot := parser.DetectBot(ua)

Result Structure

type Result struct {
    Browser   Browser  // Name, Version, Major, Family, Channel
    Engine    Engine   // Name, Version
    OS        OS       // Name, Version, Major, Minor, Patch
    CPU       CPU      // Architecture, Bits
    Device    Device   // Type, Vendor, Model, IsPhone, IsTablet, IsDesktop, IsTV, IsTouch
    App       App      // Name, Version, Kind (in-app browsers)
    Bot       Bot      // IsBot, Name, Class, Vendor, Version, IsVerified, Confidence

    IsMobile  bool     // Device is a phone
    IsDesktop bool     // Device is desktop/laptop
    IsTablet  bool     // Device is a tablet
    IsBot     bool     // Any kind of bot/crawler
    IsCrawler bool     // Specifically a search/SEO crawler
    IsInApp   bool     // In-app browser (Facebook, Instagram, etc.)
}

// Convenience methods
r.ShortBrowser() // "Chrome 123"
r.ShortOS()      // "Windows 10"
r.DeviceClass()  // "mobile", "desktop", "tablet", "smarttv", "console", "car", "wearable", "unknown"

Bot Detection

bot := parser.DetectBot(ua)
if bot.IsBot {
    fmt.Println(bot.Name)       // "GPTBot"
    fmt.Println(bot.Class)      // "ai"
    fmt.Println(bot.Vendor)     // "OpenAI"
    fmt.Println(bot.IsVerified) // false
    fmt.Println(bot.Confidence) // 1.0
}

Bot classes: search | social | ai | seo-tool | monitor | scraper | other

821 named bots including:

ClassExamples
SearchGooglebot, Bingbot, YandexBot, Baiduspider, DuckDuckBot, Applebot
SocialTwitterbot, facebookexternalhit, LinkedInBot, Slackbot, Discordbot, TelegramBot, WhatsApp
AIGPTBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, PerplexityBot, DeepSeekBot, Bytespider, CCBot, Amazonbot, Google-Extended, xAI-Bot
SEOAhrefsBot, SemrushBot, MJ12bot, DotBot, Screaming Frog, SISTRIX, Seobility
MonitorUptimeRobot, Pingdom, Datadog, NewRelic, Site24x7, GTmetrix, Uptime-Kuma, PRTG, Nagios
Scrapercurl, wget, python-requests, Scrapy, PhantomJS, HeadlessChrome, Playwright, Puppeteer, Shodan, Nmap

Plus 14 heuristic keyword patterns that catch unnamed bots.

Caching

// Simple LRU cache (32ns cache hit, 0 allocs)
cached := uax.NewCachedParser(parser, 10000)
r := cached.ParseString(ua)

// Sharded cache for high concurrency (82ns contended, 0 allocs)
sharded := uax.NewShardedCache(parser, 16, 1000) // 16 shards x 1000 entries
r := sharded.ParseString(ua)

// Stats
stats := cached.Stats() // or sharded.Stats()
fmt.Println(stats.Hits, stats.Misses, stats.Size)

Custom Rules

Override or extend builtin detection at parser creation time:

parser, _ := uax.NewParser(
    uax.WithCustomBotRules([]uax.BotRule{
        {Token: "InternalBot", Name: "Our Bot", Class: uax.BotMonitor, Vendor: "Us", Match: "exact"},
    }),
    uax.WithCustomBrowserRules([]uax.BrowserRule{
        {Token: "MyApp", Name: "My App", Family: "Chromium", Engine: "Blink", Match: "exact"},
    }),
    uax.WithCustomDeviceRules([]uax.DeviceRule{
        {Token: "MyKiosk", Type: "embedded", Vendor: "Acme", Model: "Kiosk v2", Match: "exact"},
    }),
)

Custom rules are checked before builtins, so they can override default behavior.

Hooks

parser, _ := uax.NewParser(
    uax.WithPreParseHook(func(input uax.Input) {
        log.Println("Parsing:", input.UAString[:50])
    }),
    uax.WithPostParseHook(func(input uax.Input, result uax.Result, d time.Duration) {
        metrics.ParseDuration.Observe(d.Seconds())
        if result.IsBot {
            metrics.BotRequests.Inc()
        }
    }),
)

Hooks are nil-checked — zero cost when not configured.

Client Hints

Modern Chromium browsers send reduced UA strings. Client Hints provide accurate detection:

// Manual
r := parser.Parse(uax.Input{
    UAString: req.Header.Get("User-Agent"),
    ClientHints: uax.ClientHintsFromMap(map[string]string{
        "Sec-CH-UA":                  req.Header.Get("Sec-CH-UA"),
        "Sec-CH-UA-Platform":         req.Header.Get("Sec-CH-UA-Platform"),
        "Sec-CH-UA-Platform-Version": req.Header.Get("Sec-CH-UA-Platform-Version"),
        "Sec-CH-UA-Arch":             req.Header.Get("Sec-CH-UA-Arch"),
        "Sec-CH-UA-Model":            req.Header.Get("Sec-CH-UA-Model"),
        "Sec-CH-UA-Full-Version":     req.Header.Get("Sec-CH-UA-Full-Version"),
    }),
})

// Automatic (recommended)
r := parser.ParseRequest(req)

Client Hints fields override UA-derived values. Windows 11 is detected when Sec-CH-UA-Platform-Version >= 13.

Browser Channel Detection

r := parser.ParseString("Mozilla/5.0 ... Firefox/126.0a1")
r.Browser.Channel // "nightly"

r := parser.ParseString("Mozilla/5.0 ... Firefox/125.0b9")
r.Browser.Channel // "beta"

Detected channels: nightly, beta, dev, canary (from version patterns and Client Hints brands).

Options

parser, _ := uax.NewParser(
    uax.WithBotDetection(false),                    // disable bot detection
    uax.WithCustomBrowserRules([]uax.BrowserRule{}), // custom browser rules
    uax.WithCustomBotRules([]uax.BotRule{}),         // custom bot rules
    uax.WithCustomDeviceRules([]uax.DeviceRule{}),   // custom device rules
    uax.WithPreParseHook(fn),                        // pre-parse callback
    uax.WithPostParseHook(fn),                       // post-parse callback with timing
)

Framework Middleware

Each framework example is a separate Go module (no framework deps in the main library).

net/http (stdlib)

func UAMiddleware(parser *uax.Parser) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            result := parser.ParseRequest(r)
            ctx := context.WithValue(r.Context(), uaKey, result)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Gin

func UAMiddleware(parser *uax.Parser) gin.HandlerFunc {
    return func(c *gin.Context) {
        result := parser.ParseString(c.GetHeader("User-Agent"))
        c.Set("ua_result", result)
        c.Next()
    }
}

Echo

func UAMiddleware(parser *uax.Parser) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            result := parser.ParseString(c.Request().Header.Get("User-Agent"))
            c.Set("ua_result", result)
            return next(c)
        }
    }
}

Chi

func UAMiddleware(parser *uax.Parser) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            result := parser.ParseRequest(r)
            ctx := context.WithValue(r.Context(), ctxKey{}, result)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Fiber

func UAMiddleware(parser *uax.Parser) fiber.Handler {
    return func(c *fiber.Ctx) error {
        result := parser.ParseString(c.Get("User-Agent"))
        c.Locals("ua_result", result)
        return c.Next()
    }
}

Complete working examples in examples/gin/, examples/echo/, examples/chi/, examples/fiber/.

Performance

Benchmarked on Apple M3 Pro (arm64):

Parse performance:

User-Agent Typens/opB/opallocs/op
curl (short bot)28400
Googlebot1,1211122
GPTBot (AI)1,3251442
Chrome Desktop1,8522725
Edge Desktop1,9472885
Firefox Desktop2,0271603
Android Chrome2,1502725
Safari Mobile2,5293367
Samsung TV2,5982645
Facebook In-App3,5533367

Cache performance:

Cache Typens/opallocs/op
LRU cache hit320
Sharded cache hit640
Sharded contended (parallel)820

Internal components:

Componentns/opallocs/op
Tokenizer750
Trie lookup40

Run benchmarks:

go test -bench Benchmark -benchmem -count=3

Architecture

Three-stage parsing pipeline:

UA String ──→ [Tokenizer] ──→ [Matcher] ──→ [Assembler] ──→ Result
                  │                │              │
            zero-alloc      trie (4ns)     Client Hints
            fixed buffer    + regex        merge + derive
  1. Tokenize (tokenizer.go): Zero-alloc scanner extracts product tokens (name/version) and comment blocks using a fixed [24]token buffer.
  2. Match (match_*.go, matcher.go, trie.go): Hybrid trie + linear-scan matcher. Trie handles exact-match rules in O(key-length); contains/prefix rules — plus regex rules — fall back to the indexed scan (builtin regexes are pre-compiled at codegen time; custom regexes are validated in NewParser).
  3. Assemble (merge.go): Merges Client Hints (CH takes precedence), computes convenience booleans.

Rules are defined in rules/*.yaml and compiled to Go source via cmd/uagen. Generated rules_gen_*.go files are committed — consumers never run the generator.

Coverage

CategoryCountDescription
Browsers385Chrome, Firefox, Safari, Edge, Opera, Brave, Vivaldi, Samsung Internet, Arc, Whale, DuckDuckGo, 100+ more
Engines16Blink, WebKit, Gecko, Trident, Presto, EdgeHTML, Goanna, KHTML, Servo, NetSurf, and more
Bots821Search, social, AI, SEO, monitor, scraper, security scanners, HTTP libraries
Devices76iPhones, iPads, Samsung Galaxy, Pixel, Xiaomi, consoles (PS5, Xbox, Switch), TVs, Kindle, Tesla, Apple Watch
In-App368Facebook, Instagram, TikTok, WeChat, Telegram, Slack, Teams, Spotify, and 50+ more
OS35Windows (10/11), macOS, iOS, iPadOS, Android, ChromeOS, 15+ Linux distros, HarmonyOS, Tizen, KaiOS, BSD variants
Heuristics14bot, crawler, spider, scraper, fetcher, archiver, validator, monitoring, analyzer, and more

Code Generation

Rules are maintained in YAML and compiled to Go:

cd cmd/uagen && go run .

This reads rules/*.yaml and generates rules_gen_*.go in the repo root. Generated files are committed — library consumers never run the generator or need gopkg.in/yaml.v3.

To add a new rule, edit the appropriate YAML file and regenerate:

# rules/bots.yaml
- token: "MyNewBot"
  name: "My New Bot"
  class: monitor
  vendor: "My Company"
  match: exact

Contributing

See CONTRIBUTING.md.

OpenTelemetry

The contrib/otel module provides span attribute helpers:

import uaxotel "github.com/motiv8-team/go-ua-parser/contrib/otel"

attrs := uaxotel.Attributes(result) // []attribute.KeyValue
span.SetAttributes(attrs...)

Only non-empty fields are emitted, using the http.user_agent.* attribute namespace.

License

MIT