Skip to main content

mesh_llm_analytics/
properties.rs

1//! Base properties attached to every event.
2//!
3//! These describe the build and the coarse shape of the machine. They are
4//! chosen to answer "which platforms and versions are in use" without
5//! accumulating enough detail to fingerprint one machine.
6
7use crate::event::Properties;
8use mesh_llm_build_info::{BUILD_VERSION, is_sha_build};
9
10/// Library name reported to PostHog, so events from the node are separable
11/// from anything the website or console might send later.
12pub const LIB_NAME: &str = "mesh-llm-rust";
13
14/// How this binary was built.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum BuildChannel {
17    /// A tagged release, e.g. `0.76.0`.
18    Release,
19    /// A release candidate or other pre-release, e.g. `0.76.0-rc8`.
20    Prerelease,
21    /// A build stamped with a commit sha.
22    Development,
23}
24
25impl BuildChannel {
26    #[must_use]
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::Release => "release",
30            Self::Prerelease => "prerelease",
31            Self::Development => "development",
32        }
33    }
34
35    /// Classify a build version string.
36    #[must_use]
37    pub fn classify(version: &str) -> Self {
38        if is_sha_build(version) {
39            Self::Development
40        } else if version.contains('-') {
41            Self::Prerelease
42        } else {
43            Self::Release
44        }
45    }
46}
47
48/// The operating system family, as a fixed string.
49#[must_use]
50pub const fn os_family() -> &'static str {
51    if cfg!(target_os = "macos") {
52        "macos"
53    } else if cfg!(target_os = "linux") {
54        "linux"
55    } else if cfg!(target_os = "windows") {
56        "windows"
57    } else {
58        "other"
59    }
60}
61
62/// How this process was started, as a fixed string.
63///
64/// This exists because an install identifier is one *state directory*, not
65/// one machine. A container gets a fresh `$HOME` on every run, so it mints a
66/// new identifier and reports `install_first_run` every single time — which
67/// reads as organic adoption and is not. Labelling the environment keeps
68/// those runs in the data (they are real executions) while making them
69/// separable from someone's laptop.
70///
71/// Deliberately a label and not a new opt-out: suppressing these runs would
72/// silently discard data, and the honest fix for "is this a person?" is to
73/// let the question be asked rather than answered in advance.
74///
75/// Checked most-specific first. A CI job usually runs in a container too, and
76/// "ci" is the more useful of the two answers.
77#[must_use]
78pub fn exec_env() -> &'static str {
79    if crate::consent::detect_ci() {
80        "ci"
81    } else if in_container() {
82        "container"
83    } else if in_service_manager() {
84        "service"
85    } else {
86        "plain"
87    }
88}
89
90/// Files and variables that mark a container runtime.
91///
92/// `container` is set by podman, systemd-nspawn, and LXC;
93/// `KUBERNETES_SERVICE_HOST` is injected into every pod.
94fn in_container() -> bool {
95    std::path::Path::new("/.dockerenv").exists()
96        || std::path::Path::new("/run/.containerenv").exists()
97        || env_is_set("container")
98        || env_is_set("KUBERNETES_SERVICE_HOST")
99}
100
101/// Whether a service manager started this process rather than a person.
102///
103/// `INVOCATION_ID` is set by systemd for every unit it starts. launchd has no
104/// equivalent marker, so macOS agents fall through to `plain`; that is a known
105/// blind spot rather than a claim that none exist.
106fn in_service_manager() -> bool {
107    env_is_set("INVOCATION_ID")
108}
109
110fn env_is_set(key: &str) -> bool {
111    std::env::var_os(key).is_some_and(|value| !value.is_empty())
112}
113
114/// The CPU architecture, as a fixed string.
115#[must_use]
116pub const fn architecture() -> &'static str {
117    if cfg!(target_arch = "aarch64") {
118        "aarch64"
119    } else if cfg!(target_arch = "x86_64") {
120        "x86_64"
121    } else {
122        "other"
123    }
124}
125
126/// Build the base property set for this process.
127///
128/// The version string is passed through [`crate::Label::sanitize_or_redact`]
129/// because `MESH_LLM_BUILD_VERSION` is set by the build environment rather
130/// than by this crate.
131#[must_use]
132pub fn base_properties() -> Properties {
133    Properties::new()
134        .with(
135            "mesh_llm_version",
136            crate::Label::sanitize_or_redact(BUILD_VERSION),
137        )
138        .with(
139            "build_channel",
140            BuildChannel::classify(BUILD_VERSION).as_str(),
141        )
142        .with("os", os_family())
143        .with("arch", architecture())
144        .with("exec_env", exec_env())
145        .with("$lib", LIB_NAME)
146        .with(
147            "$lib_version",
148            crate::Label::sanitize_or_redact(BUILD_VERSION),
149        )
150}
151
152#[cfg(test)]
153#[path = "properties/tests.rs"]
154mod tests;