ddgo

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 3, 2026 License: LGPL-3.0 Imports: 13 Imported by: 0

README

ddgo - Device Detector for Go

Go Report Card ci version license Go Reference

ddgo is a Go port of Matomo Device Detector.

It parses user-agent strings (and optional Client Hints) into normalized bot/client/OS/device metadata.

What it detects

  • Bot: bot flag, bot name, category, producer metadata.
  • Client: client type, name, version, engine, engine version.
  • OS: operating system name, version, and platform.
  • Device: device type, brand, and model.

Why ddgo

  • Uses upstream Matomo regex snapshot data.
  • Produces deterministic compiled artifacts (sync/compiled.json, sync/manifest.json).
  • Supports Client Hints enrichment (ParseWithClientHints, ParseWithHeaders).
  • Concurrency-safe detector usage.
  • Optional pluggable parse-result cache.

Install

go get github.com/metalagman/ddgo

Library usage

import "github.com/metalagman/ddgo"

detector, err := ddgo.New()
if err != nil {
    // handle initialization error
}
result, err := detector.Parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0")
if err != nil {
    // handle parse error
}
// result.Client.Name == "Firefox"
// result.Client.Version == "124.0"

Bot + client + OS + device fields:

result, _ := detector.Parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0")
// result.Bot.IsBot == false
// result.Client.Name == "Firefox"
// result.OS.Name == "Windows"
// result.Device.Type == "Desktop"

Bot detection:

result, _ := detector.Parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
// result.Bot.IsBot == true
// result.Bot.Name == "Googlebot"
// result.Device.Type == "Bot"

Client hints via structured input:

mobile := true
hints := ddgo.ClientHints{
    Brands:          []ddgo.ClientHintBrand{{Name: "Google Chrome", Version: "122.0.6261.128"}},
    Platform:        "Android",
    PlatformVersion: "14.0.0",
    Model:           "SM-G991B",
    Mobile:          &mobile,
}
result, _ := detector.ParseWithClientHints("Mozilla/5.0", hints)
// result.Client.Name == "Chrome"
// result.OS.Name == "Android"
// result.Device.Model == "SM-G991B"

Client hints from headers:

headers := map[string]string{
    "Sec-CH-UA":                  "\"Not(A:Brand\";v=\"99\", \"Microsoft Edge\";v=\"123.0.0.0\", \"Chromium\";v=\"123.0.0.0\"",
    "Sec-CH-UA-Platform":         "\"Windows\"",
    "Sec-CH-UA-Platform-Version": "\"15.0.0\"",
    "Sec-CH-UA-Mobile":           "?0",
}
result, _ := detector.ParseWithHeaders("Mozilla/5.0", headers)
// result.Client.Name == "Microsoft Edge"
// result.Client.Version == "123.0.0.0"
// result.Device.Type == "Desktop"

Parser options:

detector, _ = ddgo.New(ddgo.WithMaxUserAgentLen(7))
result, _ = detector.Parse("Mozilla/5.0")
// result.UserAgent == "Mozilla"

detector, _ = ddgo.New(ddgo.WithUserAgentTrimming(false))
result, _ = detector.Parse("  Mozilla/5.0  ")
// result.UserAgent == "  Mozilla/5.0  "

Parse only client hints:

headers := map[string]string{
    "Sec-CH-UA-Full-Version-List": "\"Not A;Brand\";v=\"24\", \"Chromium\";v=\"122.0.6261.128\", \"Google Chrome\";v=\"122.0.6261.128\"",
    "Sec-CH-UA-Platform":          "\"Android\"",
    "Sec-CH-UA-Mobile":            "?1",
}
hints := ddgo.ParseClientHintsFromHeaders(headers)
// len(hints.Brands) == 3
// hints.Platform == "Android"

Cache configuration:

// Preferred: choose implementation explicitly via the cache interface.
detector, _ = ddgo.New(ddgo.WithResultCache(ddgo.NewLRUResultCache(512)))

Independent caching interface:

type ResultCache interface {
    Get(key string) (ddgo.Result, bool)
    Set(key string, result ddgo.Result)
}

Built-in cache implementations:

// Bounded LRU-style cache:
detector, _ := ddgo.New(ddgo.WithResultCache(ddgo.NewLRUResultCache(512)))

// Unbounded in-memory cache:
detector, _ = ddgo.New(ddgo.WithResultCache(ddgo.NewMemoryResultCache()))

Custom cache implementation:

type myCache struct{}

func (m *myCache) Get(key string) (ddgo.Result, bool) { return ddgo.Result{}, false }
func (m *myCache) Set(key string, result ddgo.Result) {}

detector, _ := ddgo.New(ddgo.WithResultCache(&myCache{}))

Benchmark snapshot

Measured on Linux/amd64 (AMD Ryzen 7 PRO 7840U), March 3, 2026.

Scenario Throughput/Latency Memory Allocations
Parse typical desktop browser UA (BenchmarkParseFirefox) ~8.2-8.6 ms/op ~14.6-15.2 KB/op ~75-77 allocs/op
Parse common bot UA (BenchmarkParseGooglebot) ~1.34-1.37 ms/op ~1.63-1.65 KB/op 14 allocs/op
Parse with warm cache hit (BenchmarkParseCachedFirefox) ~1.10-1.15 us/op 1296 B/op 13 allocs/op
ParseWithHeaders (BenchmarkParseWithHeaders) ~1.44-1.67 ms/op ~3.6-3.7 KB/op 35 allocs/op

Run benchmarks locally:

go test -run '^$' -bench 'BenchmarkParse' -benchmem .

Full performance notes are in PERFORMANCE.md.

Examples

Runnable examples are in example_test.go (Example* functions).

Data source and sync model

  • Upstream source: matomo-org/device-detector regex definitions.
  • Snapshot mirror path: sync/current/.
  • Compiled runtime artifact: sync/compiled.json.
  • Manifest metadata is maintained for reproducibility.

Licensing

  • ddgo is licensed under LGPL-3.0-or-later (same as Matomo Device Detector).
  • License and notice references:
    • LICENSE
    • THIRD_PARTY_NOTICES.md

Documentation

Overview

Package ddgo parses user-agent strings into bot, client, operating system, and device metadata.

The parser is intentionally deterministic and returns Unknown for fields that cannot be derived from available inputs. For browser client-hint enrichment, ParseWithHeaders and ParseWithClientHints can be used alongside Parse.

Index

Examples

Constants

View Source
const Unknown = "Unknown"

Unknown is a sentinel value used when parser data is not available. Callers can compare string fields to this value instead of empty string.

Variables

View Source
var ErrNilDetector = errors.New("ddgo: nil detector")

ErrNilDetector is returned when Parse is called on a nil *Detector.

Functions

This section is empty.

Types

type Bot

type Bot struct {
	IsBot    bool
	Name     string
	Category string
	URL      string
	Producer Producer
}

Bot describes bot detection output.

type Client

type Client struct {
	Type          string
	Name          string
	Version       string
	Engine        string
	EngineVersion string
}

Client describes client application output.

type ClientHintBrand

type ClientHintBrand struct {
	Name    string
	Version string
}

ClientHintBrand represents one structured brand entry from Sec-CH-UA.

type ClientHints

type ClientHints struct {
	Brands          []ClientHintBrand
	Platform        string
	PlatformVersion string
	Model           string
	Mobile          *bool
}

ClientHints contains normalized Sec-CH-UA style client hints.

Mobile is nil when the client did not send Sec-CH-UA-Mobile.

func ParseClientHintsFromHeaders

func ParseClientHintsFromHeaders(headers map[string]string) ClientHints

ParseClientHintsFromHeaders extracts known client hints from HTTP header values. Header name matching is case-insensitive.

Example
package main

import (
	"fmt"

	"github.com/metalagman/ddgo"
)

func main() {
	headers := map[string]string{
		"Sec-CH-UA-Full-Version-List": "\"Not A;Brand\";v=\"24\", \"Chromium\";v=\"122.0.6261.128\", \"Google Chrome\";v=\"122.0.6261.128\"",
		"Sec-CH-UA-Platform":          "\"Android\"",
		"Sec-CH-UA-Mobile":            "?1",
	}

	hints := ddgo.ParseClientHintsFromHeaders(headers)
	fmt.Printf("brands=%d platform=%s mobile=%t\n", len(hints.Brands), hints.Platform, *hints.Mobile)
}
Output:
brands=3 platform=Android mobile=true

type Detector

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

Detector parses user-agent strings into structured detection results.

A Detector is safe for concurrent use by multiple goroutines.

func New

func New(opts ...Option) (*Detector, error)

New creates a detector with optional configuration overrides.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New()
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	result, err := detector.Parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0")
	if err != nil {
		log.Fatalf("Parse() failed: %v", err)
	}

	fmt.Printf("%s %s\n", result.Client.Name, result.Client.Version)
}
Output:
Firefox 124.0

func (*Detector) Parse

func (d *Detector) Parse(userAgent string) (Result, error)

Parse analyzes a user-agent string and returns a detection result.

Parse can return cached results for identical normalized user-agent inputs.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New()
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	result, err := detector.Parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
	if err != nil {
		log.Fatalf("Parse() failed: %v", err)
	}

	fmt.Printf("is_bot=%t name=%s device=%s\n", result.Bot.IsBot, result.Bot.Name, result.Device.Type)
}
Output:
is_bot=true name=Googlebot device=Bot

func (*Detector) ParseWithClientHints

func (d *Detector) ParseWithClientHints(userAgent string, hints ClientHints) (Result, error)

ParseWithClientHints analyzes a user-agent string with explicit client hints.

Hint-based parsing bypasses the internal Parse cache.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New()
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	mobile := true
	hints := ddgo.ClientHints{
		Brands:          []ddgo.ClientHintBrand{{Name: "Google Chrome", Version: "122.0.6261.128"}},
		Platform:        "Android",
		PlatformVersion: "14.0.0",
		Model:           "SM-G991B",
		Mobile:          &mobile,
	}

	result, err := detector.ParseWithClientHints("Mozilla/5.0", hints)
	if err != nil {
		log.Fatalf("ParseWithClientHints() failed: %v", err)
	}
	fmt.Printf("%s %s on %s (%s)\n", result.Client.Name, result.Client.Version, result.OS.Name, result.Device.Model)
}
Output:
Chrome 122.0.6261.128 on Android (SM-G991B)

func (*Detector) ParseWithHeaders

func (d *Detector) ParseWithHeaders(userAgent string, headers map[string]string) (Result, error)

ParseWithHeaders analyzes a user-agent string and Sec-CH-UA style headers.

This helper normalizes headers via ParseClientHintsFromHeaders and then delegates to ParseWithClientHints.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New()
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	headers := map[string]string{
		"Sec-CH-UA":                  "\"Not(A:Brand\";v=\"99\", \"Microsoft Edge\";v=\"123.0.0.0\", \"Chromium\";v=\"123.0.0.0\"",
		"Sec-CH-UA-Platform":         "\"Windows\"",
		"Sec-CH-UA-Platform-Version": "\"15.0.0\"",
		"Sec-CH-UA-Mobile":           "?0",
	}

	result, err := detector.ParseWithHeaders("Mozilla/5.0", headers)
	if err != nil {
		log.Fatalf("ParseWithHeaders() failed: %v", err)
	}
	fmt.Printf("%s %s (%s)\n", result.Client.Name, result.Client.Version, result.Device.Type)
}
Output:
Microsoft Edge 123.0.0.0 (Desktop)

type Device

type Device struct {
	Type  string
	Brand string
	Model string
}

Device describes detected device output.

type LRUResultCache

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

LRUResultCache is a bounded in-memory LRU cache implementation.

func NewLRUResultCache

func NewLRUResultCache(capacity int) *LRUResultCache

NewLRUResultCache creates a bounded in-memory LRU parse result cache.

Returns nil when capacity is <= 0.

func (*LRUResultCache) Get

func (c *LRUResultCache) Get(key string) (Result, bool)

Get returns a cached result for key and updates recency on hit.

func (*LRUResultCache) Set

func (c *LRUResultCache) Set(key string, result Result)

Set stores a result for key and evicts the least-recently-used entry when full.

type MemoryResultCache

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

MemoryResultCache is a simple unbounded in-memory cache implementation.

func NewMemoryResultCache

func NewMemoryResultCache() *MemoryResultCache

NewMemoryResultCache creates an unbounded in-memory cache.

func (*MemoryResultCache) Get

func (c *MemoryResultCache) Get(key string) (Result, bool)

Get returns a cached result for key.

func (*MemoryResultCache) Set

func (c *MemoryResultCache) Set(key string, result Result)

Set stores a cached result for key.

type OS

type OS struct {
	Name     string
	Version  string
	Platform string
}

OS describes operating system output.

type Option

type Option func(*options)

Option configures Detector behavior.

func WithMaxUserAgentLen

func WithMaxUserAgentLen(limit int) Option

WithMaxUserAgentLen limits how many bytes of a user-agent string are parsed. Values below 1 are ignored.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New(ddgo.WithMaxUserAgentLen(7))
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	result, err := detector.Parse("Mozilla/5.0")
	if err != nil {
		log.Fatalf("Parse() failed: %v", err)
	}

	fmt.Println(result.UserAgent)
}
Output:
Mozilla

func WithResultCache

func WithResultCache(cache ResultCache) Option

WithResultCache configures a custom parse result cache implementation.

Passing nil explicitly disables caching.

func WithUserAgentTrimming

func WithUserAgentTrimming(enabled bool) Option

WithUserAgentTrimming toggles normalization of user-agent whitespace.

When enabled, Parse collapses repeated whitespace and trims leading/trailing space before matching.

Example
package main

import (
	"fmt"
	"log"

	"github.com/metalagman/ddgo"
)

func main() {
	detector, err := ddgo.New(ddgo.WithUserAgentTrimming(false))
	if err != nil {
		log.Fatalf("ddgo.New() failed: %v", err)
	}
	result, err := detector.Parse("  Mozilla/5.0  ")
	if err != nil {
		log.Fatalf("Parse() failed: %v", err)
	}

	fmt.Printf("%q\n", result.UserAgent)
}
Output:
"  Mozilla/5.0  "

type Producer

type Producer struct {
	Name string
	URL  string
}

Producer stores metadata about a bot vendor.

type Result

type Result struct {
	UserAgent string
	Bot       Bot
	Client    Client
	OS        OS
	Device    Device
}

Result is the aggregate detection output returned by Parse variants.

type ResultCache

type ResultCache interface {
	Get(key string) (Result, bool)
	Set(key string, result Result)
}

ResultCache is a pluggable cache used by Detector.Parse.

Implementations must be safe for concurrent use.

Directories

Path Synopsis
cmd
ddsync command
Command ddsync builds and verifies deterministic sync artifacts.
Command ddsync builds and verifies deterministic sync artifacts.
ddsync/cmd
Package cmd provides the Cobra command tree for the ddsync CLI.
Package cmd provides the Cobra command tree for the ddsync CLI.
internal
ddsync
Package ddsync implements deterministic snapshot compilation and verification.
Package ddsync implements deterministic snapshot compilation and verification.

Jump to

Keyboard shortcuts

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