herald

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 8 Imported by: 0

README

herald

Fast, zero-dependency Go library for parsing User-Agent strings.

Detects browsers, operating systems, devices, bots, in-app browsers (Facebook, Instagram, TikTok), and native HTTP clients. Supports Client Hints for modern browsers with frozen UA strings.

Features

  • Browser detection — Chrome, Safari, Firefox, Edge, Opera, Yandex, Samsung Browser, IE, and more
  • OS detection — iOS, Android, Windows (NT version mapping), macOS, Linux, Chrome OS, Darwin
  • Device detection — type (mobile/tablet/desktop) + model resolution via Apple and Android lookup tables
  • Bot detection — byte-level trie for ~100 known bots + feature scoring for unknown bots
  • In-app browser parsing — Facebook (FBAN), Instagram (positional), Threads (Barcelona), TikTok (musical_ly), Meta IAB
  • Client HintsSec-CH-UA headers enrich or override frozen UA data
  • Data-driven — bot patterns and device models embedded as JSON, extensible via WithOverrides
  • Zero external dependencies — stdlib only

Install

go get github.com/withoutasecondthought/herald

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/withoutasecondthought/herald"
)

func main() {
    // Zero-config: uses embedded data, no file paths needed.
    p, err := herald.NewParser()
    if err != nil {
        log.Fatal(err)
    }

    ua := "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    r := p.Parse(ua)

    fmt.Println(r.Browser.Name)    // Chrome
    fmt.Println(r.Browser.Version) // 120.0.0.0
    fmt.Println(r.Browser.Engine)  // Blink
    fmt.Println(r.OS.Name)         // Windows
    fmt.Println(r.OS.Version)      // 10
    fmt.Println(r.Device.Type)     // desktop
    fmt.Println(r.ClientType)      // 0 (ClientTypeBrowser)
}

Client Hints

Modern browsers freeze the UA string. Use ParseWithHints to get accurate data from Sec-CH-UA headers:

hints := herald.ClientHints{
    UA:              `"Chromium";v="120", "Google Chrome";v="120"`,
    Platform:        "Windows",
    PlatformVersion: "15.0.0", // Windows 11
    Mobile:          false,
}

r := p.ParseWithHints(ua, hints)
fmt.Println(r.OS.Version) // 11 (resolved from PlatformVersion)

Fast Type Detection

When you only need to know what kind of client it is (bot vs browser vs native app) without full parsing:

switch p.DetectType(ua) {
case herald.ClientTypeBot:
    // block or rate-limit
case herald.ClientTypeBrowser:
    // serve page
case herald.ClientTypeHttpClient:
    // API client
}

DetectType skips browser/OS/device resolution — bot detection runs in ~160ns with zero allocations.

Custom Data

By default, NewParser() uses built-in data embedded at compile time. You can extend or replace it:

// Add your own bot patterns or device models on top of built-in data.
// The override directory may contain any subset of the data files.
// Only files that exist are merged; missing files are skipped.
p, err := herald.NewParser(herald.WithOverrides("path/to/overrides"))

// Use only your own data files (ignores built-in data entirely).
p, err := herald.NewParser(herald.WithDataDir("path/to/data"))
File Formats

bots.json — array of bot patterns (added to the built-in trie):

[
  {"pattern": "MyBot", "name": "My Bot", "owner": "Acme", "category": "scraper"}
]

pattern is matched as a substring in the UA string (case-sensitive). category is one of: search, ai, social, monitor, scraper, other.

android.json — map of model identifier to brand and name:

{
  "SM-G991B": {"brand": "Samsung", "model": "Galaxy S21"},
  "Pixel 8":  {"brand": "Google",  "model": "Pixel 8"}
}

apple.json — map of internal identifier to device name:

{
  "iPhone14,2": "iPhone 13 Pro",
  "iPad13,1":   "iPad Air (4th generation)"
}

darwin.json — map of Darwin kernel major version to OS versions:

{
  "24": {"ios": "18", "macos": "15"},
  "25": {"ios": "19", "macos": "16"}
}

Package-Level API

For simpler setups, use the package-level functions with a shared default parser:

herald.Init() // uses embedded data

r := herald.Parse(ua)
r := herald.ParseWithHints(ua, hints)
t := herald.DetectType(ua)

Result Types

Parse returns a *Result with these fields:

type Result struct {
    Raw        string
    ClientType ClientType
    Browser    Browser   // Name, Version, Engine
    OS         OS        // Name, Version, Build
    Device     Device    // Type, Model, ModelRaw
    IAB        IABInfo   // App, AppVersion, Locale, ScreenScale, ...
    Bot        BotInfo   // Name, Owner, Category, Confidence
    Native     NativeApp // Name, Version, Runtime
}

All sub-types are values (not pointers) with an IsEmpty() method:

if !r.Browser.IsEmpty() {
    fmt.Println(r.Browser.Name)
}
Browser
Field Example
Name "Chrome", "Safari", "Firefox", "Edge"
Version "120.0.0.0"
Engine "Blink", "WebKit", "Gecko", "Trident"
OS
Field Example
Name "iOS", "Android", "Windows", "macOS", "Linux"
Version "18.7", "14", "10"
Build "23A355" — real iOS build from the Mobile/ token (WebView UAs); empty outside iOS and for Safari's frozen 15E148
Device
Field Example
Type "mobile", "tablet", "desktop"
Model "iPhone 13 Pro", "Galaxy S24 Ultra" (resolved from DB)
ModelRaw "iPhone14,2", "SM-S928B" (raw identifier from UA)
BotInfo
Field Example
Name "Googlebot", "GPTBot", "ClaudeBot"
Owner "Google", "OpenAI", "Anthropic"
Category "search", "ai", "social", "monitor", "scraper"
Confidence 1.0 (trie match) or 0.0-1.0 (scoring)
IABInfo (In-App Browser)
Field Description
App App name: "Facebook", "Instagram", "Threads", "TikTok", "Meta"
AppVersion App version string
Locale e.g. "en_US"
ScreenScale Screen density factor
Resolution Screen resolution (Instagram/TikTok)
Region Region override (Instagram, Threads, TikTok)
NetType Network type (TikTok)
NativeApp
Field Example
Name "curl", "Dart", "OkHttp", "CFNetwork"
Version "7.68.0"
Runtime "dart:io", "OkHttp", "CFNetwork"

Client Types

ClientType Populated fields Example
ClientTypeBrowser Browser, OS, Device Chrome, Safari, Firefox
ClientTypeIAB Browser, OS, Device, IAB Facebook app, Instagram app
ClientTypeNativeApp Native, OS, Device CFNetwork, OkHttp, Dart
ClientTypeHttpClient Native curl, wget, python-requests
ClientTypeBot Bot Googlebot, GPTBot
ClientTypeUnknown (none) Empty UA string

Data Files

The data/ directory contains JSON databases embedded into the binary at compile time:

File Entries Description
bots.json ~100 Known bot patterns with name, owner, and category
apple.json ~180 Apple device identifiers (e.g. iPhone14,2 -> iPhone 13 Pro)
android.json ~300 Android models: Samsung, Google, Xiaomi, OPPO, vivo, OnePlus, Realme, Motorola, Sony, Nothing, Infinix, TECNO, Huawei
darwin.json ~13 Darwin kernel version -> iOS/macOS version mapping

To add your own entries, use WithOverrides to merge a directory of JSON files on top of the built-in data.

Architecture

Pipeline order: empty check -> bot detection (trie, then scoring) -> client type classification -> browser -> OS -> device -> IAB -> Client Hints enrichment.

Bot detection runs first — if a bot is found, all other stages are skipped.

Performance

Benchmarks on Apple M3 Max, Go 1.26:

Benchmark ns/op B/op allocs/op
Parse (Chrome Desktop) 5,600 3,856 14
Parse (Safari iOS) 6,700 3,856 14
Parse (Googlebot) 770 1,184 4
Parse (CFNetwork) 2,100 3,512 7
Parse (empty) 74 384 1
DetectType (Chrome) 2,070 944 6
DetectType (Googlebot) 159 0 0

Run benchmarks yourself:

go test -bench=. -benchmem ./...

License

MIT

Documentation

Index

Constants

View Source
const (
	OSiOS      = "iOS"
	OSAndroid  = "Android"
	OSWindows  = "Windows"
	OSmacOS    = "macOS"
	OSLinux    = "Linux"
	OSDarwin   = "Darwin"
	OSChromeOS = "Chrome OS"
)

OS name constants.

View Source
const (
	DeviceMobile  = "mobile"
	DeviceTablet  = "tablet"
	DeviceDesktop = "desktop"
)

Device type constants.

View Source
const (
	EngineBlink   = "Blink"
	EngineWebKit  = "WebKit"
	EngineGecko   = "Gecko"
	EngineTrident = "Trident"
	EnginePresto  = "Presto"
)

Browser engine constants.

View Source
const (
	ProductCFNetwork = "CFNetwork"
	ProductDarwin    = "Darwin"
	ProductMozilla   = "Mozilla"
)

Well-known product/browser token names.

Variables

This section is empty.

Functions

func Init

func Init(opts ...Option) error

Init initializes the default parser for the package-level Parse/ParseWithHints/DetectType.

Without options, it uses embedded defaults:

herald.Init()

With options:

herald.Init(herald.WithOverrides("path/to/extra"))

Types

type BotCategory

type BotCategory string

BotCategory classifies what kind of bot this is.

const (
	BotCategorySearch  BotCategory = "search"
	BotCategorySocial  BotCategory = "social"
	BotCategoryAI      BotCategory = "ai"
	BotCategoryMonitor BotCategory = "monitor"
	BotCategoryScraper BotCategory = "scraper"
)

type BotInfo

type BotInfo struct {
	Name       string
	Owner      string
	Category   BotCategory
	Confidence float64 // 0.0-1.0
}

BotInfo holds information about a detected bot.

func (BotInfo) IsEmpty

func (b BotInfo) IsEmpty() bool

IsEmpty returns true if no bot was detected.

type Browser

type Browser struct {
	Name    string // "Chrome", "Safari", "Firefox"
	Version string // "120.0.0.0"
	Engine  string // "Blink", "WebKit", "Gecko"
}

Browser holds parsed browser information.

func (Browser) IsEmpty

func (b Browser) IsEmpty() bool

IsEmpty returns true if no browser was detected.

type ClientHints

type ClientHints struct {
	UA              string // Sec-CH-UA: "Chromium";v="120", "Google Chrome";v="120"
	Mobile          bool   // Sec-CH-UA-Mobile: ?0
	Platform        string // Sec-CH-UA-Platform: "Windows"
	PlatformVersion string // Sec-CH-UA-Platform-Version: "15.0.0"
	FullVersionList string // Sec-CH-UA-Full-Version-List
	Model           string // Sec-CH-UA-Model
	Architecture    string // Sec-CH-UA-Arch
}

ClientHints represents the Client Hints headers sent by modern browsers as a replacement for the frozen User-Agent string. These values take priority over data parsed from the UA string.

func (ClientHints) IsEmpty

func (ch ClientHints) IsEmpty() bool

type ClientType

type ClientType uint8

ClientType indicates the category of the user agent. Each type implies which fields in Result are populated:

ClientTypeBrowser   — Browser + OS + Device
ClientTypeIAB       — Browser + OS + Device + IAB
ClientTypeNativeApp — OS + Device + Native
ClientTypeHttpClient — Native only
ClientTypeBot       — Bot only
ClientTypeUnknown   — all fields empty
const (
	ClientTypeBrowser    ClientType = iota // Browser + OS + Device
	ClientTypeIAB                          // Browser + OS + Device + IAB
	ClientTypeNativeApp                    // OS + Device + Native
	ClientTypeHttpClient                   // Native only
	ClientTypeBot                          // Bot only
	ClientTypeUnknown                      // all fields empty
)

func DetectType

func DetectType(ua string) ClientType

DetectType detects the client type. Init must be called first.

type Device

type Device struct {
	Type     string // "mobile", "tablet", "desktop", "tv", "console"
	Model    string // "iPhone 13 Pro" — may be empty
	ModelRaw string // "iPhone14,2", "SM-G991B" — raw identifier
}

Device holds parsed device information.

func (Device) IsEmpty

func (d Device) IsEmpty() bool

IsEmpty returns true if no device was detected.

type IABInfo

type IABInfo struct {
	App         string
	AppVersion  string
	Locale      string
	Region      string
	NetType     string
	ScreenScale float64
	Resolution  string
}

IABInfo holds in-app browser metadata from Facebook/Instagram/TikTok UAs.

func (IABInfo) IsEmpty

func (i IABInfo) IsEmpty() bool

IsEmpty returns true if no IAB info was detected.

type NativeApp

type NativeApp struct {
	Name    string // "curl", "Dart", "CFNetwork", "ut-1"
	Version string
	Runtime string // "dart:io", "CFNetwork", "OkHttp"
}

NativeApp holds information about a native HTTP client or app.

func (NativeApp) IsEmpty

func (n NativeApp) IsEmpty() bool

IsEmpty returns true if no native app was detected.

type OS

type OS struct {
	Name    string // "iOS", "Android", "Windows", "macOS"
	Version string // "18.7", "14", "11"
	Build   string // "23A355" — real iOS build from the Mobile/ token; empty outside iOS and for frozen Safari
}

OS holds parsed operating system information.

func (OS) IsEmpty

func (o OS) IsEmpty() bool

IsEmpty returns true if no OS was detected.

type Option

type Option func(*parserConfig)

Option configures how NewParser loads data.

func WithDataDir

func WithDataDir(dir string) Option

WithDataDir loads data exclusively from the given directory, ignoring embedded defaults. Use this when you maintain your own complete set of data files.

func WithOverrides

func WithOverrides(dir string) Option

WithOverrides merges user data on top of embedded defaults. The override directory may contain any subset of the data files (bots.json, apple.json, android.json, darwin.json). Only files that exist are merged; missing files are skipped. For device/bot data, override entries are added to or replace the built-in entries.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser holds the in-memory database and pipeline for UA parsing. Create with NewParser, then call Parse/ParseWithHints/DetectType.

func NewParser

func NewParser(opts ...Option) (*Parser, error)

NewParser creates a ready-to-use parser.

Without options, it uses the embedded default data (zero-config):

p, err := herald.NewParser()

With WithDataDir, it loads data exclusively from the given directory:

p, err := herald.NewParser(herald.WithDataDir("path/to/data"))

With WithOverrides, it uses embedded defaults and merges user data on top:

p, err := herald.NewParser(herald.WithOverrides("path/to/overrides"))

func (*Parser) Database

func (p *Parser) Database() *db.Database

Database returns the loaded database for direct access if needed.

func (*Parser) DetectType

func (p *Parser) DetectType(ua string) ClientType

DetectType is a fast path that only determines the client type without full parsing of browser/OS/device details.

func (*Parser) LookupAndroidModel

func (p *Parser) LookupAndroidModel(modelID string) (db.AndroidDevice, bool)

LookupAndroidModel looks up an Android model (e.g., "SM-G991B") → brand + model.

func (*Parser) LookupAppleModel

func (p *Parser) LookupAppleModel(modelID string) (string, bool)

LookupAppleModel looks up an Apple model identifier (e.g., "iPhone14,2") → human name.

func (*Parser) Parse

func (p *Parser) Parse(ua string) *Result

Parse parses a User-Agent string using the Parser's in-memory database.

func (*Parser) ParseWithHints

func (p *Parser) ParseWithHints(ua string, hints ClientHints) *Result

ParseWithHints parses a UA string and enriches the result with Client Hints.

type Result

type Result struct {
	Raw        string
	ClientType ClientType
	Browser    Browser
	OS         OS
	Device     Device
	IAB        IABInfo
	Bot        BotInfo
	Native     NativeApp
}

Result is the parsed representation of a User-Agent string. All fields are value types — use IsEmpty() to check if a section was populated.

func Parse

func Parse(ua string) *Result

Parse parses a UA string using the default parser. Init must be called first.

func ParseWithHints

func ParseWithHints(ua string, hints ClientHints) *Result

ParseWithHints parses a UA string with Client Hints. Init must be called first.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL