systemprompt-analytics 0.7.0

Analytics for systemprompt.io AI governance infrastructure. Session, agent, tool, and microdollar-precision cost attribution across the MCP governance pipeline.
Documentation
pub fn parse_user_agent(ua: &str) -> (Option<String>, Option<String>, Option<String>) {
    let ua_lower = ua.to_lowercase();

    let device_type = Some(parse_device_type(&ua_lower));
    let browser = Some(parse_browser(&ua_lower));
    let os = Some(parse_os(&ua_lower));

    (device_type, browser, os)
}

fn parse_device_type(ua_lower: &str) -> String {
    const MOBILE: &[&str] = &["mobile", "android", "iphone"];
    const TABLET: &[&str] = &["tablet", "ipad"];
    if MOBILE.iter().any(|s| ua_lower.contains(s)) {
        "mobile".to_string()
    } else if TABLET.iter().any(|s| ua_lower.contains(s)) {
        "tablet".to_string()
    } else {
        "desktop".to_string()
    }
}

struct BrowserRule {
    name: &'static str,
    needles: &'static [&'static str],
    negative: &'static [&'static str],
}

const BROWSER_RULES: &[BrowserRule] = &[
    BrowserRule {
        name: "Edge",
        needles: &["edg/", "edge"],
        negative: &[],
    },
    BrowserRule {
        name: "Samsung Internet",
        needles: &["samsungbrowser"],
        negative: &[],
    },
    BrowserRule {
        name: "UC Browser",
        needles: &["ucbrowser", "ucweb"],
        negative: &[],
    },
    BrowserRule {
        name: "Yandex",
        needles: &["yabrowser"],
        negative: &[],
    },
    BrowserRule {
        name: "QQ Browser",
        needles: &["qqbrowser"],
        negative: &[],
    },
    BrowserRule {
        name: "WeChat",
        needles: &["micromessenger"],
        negative: &[],
    },
    BrowserRule {
        name: "Silk",
        needles: &["silk/"],
        negative: &[],
    },
    BrowserRule {
        name: "Electron",
        needles: &["electron"],
        negative: &[],
    },
    BrowserRule {
        name: "WebView",
        needles: &["cordova", "wv)"],
        negative: &[],
    },
    BrowserRule {
        name: "Chrome",
        needles: &["chrome"],
        negative: &["edg"],
    },
    BrowserRule {
        name: "Firefox",
        needles: &["firefox"],
        negative: &[],
    },
    BrowserRule {
        name: "Safari",
        needles: &["safari"],
        negative: &["chrome"],
    },
    BrowserRule {
        name: "Opera",
        needles: &["opera", "opr/"],
        negative: &[],
    },
    BrowserRule {
        name: "IE",
        needles: &["msie", "trident"],
        negative: &[],
    },
    BrowserRule {
        name: "Brave",
        needles: &["brave"],
        negative: &[],
    },
    BrowserRule {
        name: "Vivaldi",
        needles: &["vivaldi"],
        negative: &[],
    },
    BrowserRule {
        name: "DuckDuckGo",
        needles: &["duckduckgo"],
        negative: &[],
    },
    BrowserRule {
        name: "Arc",
        needles: &["arc/"],
        negative: &[],
    },
];

fn parse_browser(ua_lower: &str) -> String {
    BROWSER_RULES
        .iter()
        .find(|r| {
            r.needles.iter().any(|n| ua_lower.contains(n))
                && !r.negative.iter().any(|n| ua_lower.contains(n))
        })
        .map_or_else(|| "Other".to_string(), |r| r.name.to_string())
}

const OS_RULES: &[(&str, &[&str])] = &[
    ("Windows", &["windows"]),
    ("macOS", &["mac os x", "macos"]),
    ("Android", &["android"]),
    ("iOS", &["iphone", "ipad", "ios"]),
    ("Linux", &["linux"]),
    ("ChromeOS", &["cros", "chrome os"]),
    ("BSD", &["freebsd", "openbsd"]),
];

fn parse_os(ua_lower: &str) -> String {
    OS_RULES
        .iter()
        .find(|(_, needles)| needles.iter().any(|n| ua_lower.contains(n)))
        .map_or_else(|| "Other".to_string(), |(name, _)| (*name).to_string())
}