PhoenixKitWebAnalytics.UserAgent (PhoenixKitWebAnalytics v0.2.0)

Copy Markdown View Source

A deliberately small User-Agent classifier: browser, OS, device class, and "does this look automated".

Why not a UA-parsing library

Full parsers ship a regex database of thousands of entries and refresh it on a release cadence. This module recognises the families that actually show up in aggregate reports and labels everything else "Other" — which is the honest answer for a long tail that would otherwise be a hundred one-row entries in the browsers table. No dependency, no database to keep current, and the parse is a handful of regex matches on a string we already have.

What it deliberately does not do

This is classification, not fingerprinting. Nothing here is combined with anything else to make a durable identifier: visitor_id is built from a daily-rotating salted hash (see PhoenixKitWebAnalytics.Visitor), and the raw User-Agent string is never stored.

iex> PhoenixKitWebAnalytics.UserAgent.parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36").browser
"Chrome"

iex> PhoenixKitWebAnalytics.UserAgent.parse("Googlebot/2.1 (+http://www.google.com/bot.html)").bot?
true

Summary

Functions

Whether the User-Agent looks automated.

Parses a User-Agent header.

Types

t()

@type t() :: %{
  browser: String.t(),
  browser_version: String.t() | nil,
  os: String.t(),
  os_version: String.t() | nil,
  device_type: String.t(),
  bot?: boolean()
}

Functions

bot?(user_agent)

@spec bot?(String.t() | nil) :: boolean()

Whether the User-Agent looks automated.

Accepts the raw header; matching is case-insensitive.

parse(user_agent)

@spec parse(String.t() | nil) :: t()

Parses a User-Agent header.

An empty or missing header yields all-"Unknown" with bot?: false — a client that sends no UA is unusual but not necessarily automated, and guessing "bot" there would silently drop real traffic.