events: add UA parser (device_type/os/browser)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Arlind Ukshini 2026-04-27 10:43:42 +02:00
parent 51b0508561
commit d974e865c2

51
src/ua.js Normal file
View file

@ -0,0 +1,51 @@
// ─────────────────────────────────────────────────────────────
// src/ua.js — minimal User-Agent parser.
//
// Coarse-grained classification only: device_type / os / browser.
// We deliberately do NOT pull in ua-parser-js — the project keeps a
// small dependency footprint and we only need three buckets. The raw
// UA is also stored alongside parsed fields (see src/events.js) so a
// regex miss can be re-classified later.
//
// Also owns MOBILE_UA_RE — the existing /timeline view-dispatch
// regex used by server.js. Single source of truth.
// ─────────────────────────────────────────────────────────────
// UA substrings that mean "phone-class small screen". Tablets (iPad,
// Android tablets) deliberately do NOT match — they get the desktop
// view, which matches existing behaviour in server.js.
export const MOBILE_UA_RE =
/\b(iPhone|iPod|Android.*Mobile|Mobile.*Firefox|IEMobile|BlackBerry|Opera Mini)\b/i;
// Tablet-class devices. Order matters in parseUA(): tablet check runs
// before mobile so "iPad" doesn't accidentally fall through to desktop.
const TABLET_UA_RE = /\b(iPad|Android(?!.*Mobile))\b/i;
export function parseUA(ua) {
if (!ua || typeof ua !== 'string') {
return { device_type: null, os: null, browser: null };
}
// device_type
let device_type = 'desktop';
if (TABLET_UA_RE.test(ua)) device_type = 'tablet';
else if (MOBILE_UA_RE.test(ua)) device_type = 'mobile';
// os
let os = 'other';
if (/\b(iPhone|iPad|iPod)\b/.test(ua)) os = 'iOS';
else if (/\bAndroid\b/.test(ua)) os = 'Android';
else if (/\bWindows\b/.test(ua)) os = 'Windows';
else if (/Mac OS X|Macintosh/.test(ua)) os = 'macOS';
else if (/\bLinux\b/.test(ua)) os = 'Linux';
// browser — order matters. All Chromium UAs include "Safari/", all
// Edge UAs include "Chrome/", so the most specific token must win.
let browser = 'other';
if (/\bEdg\//.test(ua)) browser = 'Edge';
else if (/\bFirefox\//.test(ua)) browser = 'Firefox';
else if (/\bChrome\//.test(ua)) browser = 'Chrome';
else if (/\bSafari\//.test(ua)) browser = 'Safari';
return { device_type, os, browser };
}