Skip to main content

cognee_telemetry/
lib.rs

1//! Cognee product-analytics client (`send_telemetry`).
2//!
3//! Mirrors Python's `cognee.shared.utils.send_telemetry`. Fires a
4//! single fire-and-forget HTTP POST to `https://test.prometh.ai` for
5//! every public API call so the cognee maintainers have an aggregate
6//! view of how the SDK is exercised.
7//!
8//! Enabled by default (locked decision 1 — Python parity). The runtime
9//! check happens **before** any identity derivation or HTTP code path,
10//! so disabling at runtime costs zero.
11//!
12//! See [`docs/observability/send_telemetry.md`][user-doc] for the full
13//! operator-facing reference (payload schema, salt rotation, privacy
14//! notes, troubleshooting).
15//!
16//! [user-doc]: https://github.com/topoteretes/cognee-rs/blob/main/docs/observability/send_telemetry.md
17//!
18//! # Quick start
19//!
20//! ```no_run
21//! # #[cfg(feature = "telemetry")] {
22//! use cognee_telemetry::send_telemetry;
23//! use serde_json::json;
24//!
25//! send_telemetry(
26//!     "cognee.forget",
27//!     "user-id-string",
28//!     Some(json!({ "endpoint": "POST /api/v1/forget" })),
29//! );
30//! # }
31//! ```
32//!
33//! # Opt-out
34//!
35//! At runtime: `TELEMETRY_DISABLED=1` (any non-empty value) or
36//! `ENV=test` / `ENV=dev`.
37//!
38//! At compile time: build `cognee-lib` (or any consumer) with
39//! `--no-default-features`. [`send_telemetry`] and
40//! [`try_send_telemetry`] still exist in the public surface but are
41//! compiled to noop bodies — no `reqwest`, no `tokio` runtime
42//! fallback, no PBKDF2 cost.
43//!
44//! # Environment variables
45//!
46//! | Var | Default | Effect |
47//! |---|---|---|
48//! | `TELEMETRY_DISABLED` | unset | Any non-empty value disables. Read on every call. |
49//! | `ENV` | unset | If `test` or `dev`, disables. Read on every call. |
50//! | `LLM_API_KEY` | unset | Source of `api_key_tracking_id` (locked decision 11 — read at every event-emission, never cached). |
51//! | `TRACKING_ID` | unset | Override `anonymous_id`. |
52//! | `TELEMETRY_API_KEY_TRACKING_SALT` | `cognee.telemetry.api-key-tracking.v1` | PBKDF2 salt override (locked decision 12). |
53//! | `TELEMETRY_REQUEST_TIMEOUT` | `5` | HTTP timeout in seconds. Clamped to `[1, 60]`. Read once per process. |
54//!
55//! # Logging
56//!
57//! All diagnostics use the `cognee.telemetry` tracing target. Enable
58//! with `RUST_LOG=cognee.telemetry=debug`.
59
60#![deny(rust_2018_idioms)]
61#![warn(missing_docs)]
62
63use thiserror::Error;
64use uuid::Uuid;
65
66#[cfg(feature = "telemetry")]
67mod client;
68#[cfg(not(feature = "telemetry"))]
69mod noop;
70#[cfg(feature = "telemetry")]
71mod real;
72
73pub mod env;
74pub mod ids;
75pub mod payload;
76pub mod sanitize;
77
78/// Property value type for `additional_properties`. Resolves to
79/// `serde_json::Value` when the `telemetry` feature is on, or `()`
80/// when it is off. Keep all public signatures referring to this alias
81/// rather than `serde_json::Value` directly so the surface compiles
82/// under `--no-default-features`.
83///
84/// Callers should pass a `Value::Object` — non-object values are
85/// silently dropped at sanitization time with a `cognee.telemetry`
86/// debug log. Reserved keys (`time`, `user_id`, `anonymous_id`,
87/// `persistent_id`, `api_key_tracking_id`, `api_key_hash`,
88/// `sdk_runtime`, `cognee_version`) MUST NOT appear in the object.
89#[cfg(feature = "telemetry")]
90pub use serde_json::Value as PropertyValue;
91
92/// Placeholder property type used when the `telemetry` feature is
93/// disabled. Replaced by `serde_json::Value` once the feature is on.
94///
95/// Public-API callers should hold values as `Option<PropertyValue>`
96/// so the same code compiles in both feature states.
97#[cfg(not(feature = "telemetry"))]
98pub type PropertyValue = ();
99
100/// Errors returned by [`try_send_telemetry`].
101///
102/// In practice, [`try_send_telemetry`] always returns `Ok(())` today —
103/// transport, serialization and proxy errors are swallowed at debug
104/// level (`cognee.telemetry` target) to preserve fire-and-forget
105/// semantics. The error variant exists so future failure modes
106/// (e.g. backpressure rejection) can be surfaced without a breaking
107/// change.
108#[derive(Debug, Error)]
109pub enum TelemetryError {
110    /// The dispatcher could not acquire a tokio runtime and the
111    /// fallback runtime build failed. Practically unreachable.
112    #[error("could not bootstrap a tokio runtime to dispatch event")]
113    NoRuntime,
114}
115
116/// Reference type for the `user_id` field. Accepts a `Uuid`, a
117/// `&str`, or `Option<Uuid>` via the [`From`] impls below; callers
118/// rarely construct this directly.
119///
120/// The chosen variant is serialized to a string in the wire payload:
121/// [`UserIdRef::Uuid`] becomes the canonical hyphenated UUID,
122/// [`UserIdRef::Symbolic`] passes through as-is, and
123/// [`UserIdRef::None`] becomes the empty string.
124#[derive(Debug, Clone)]
125pub enum UserIdRef<'a> {
126    /// A real cognee `User.id`.
127    Uuid(Uuid),
128    /// A symbolic identifier (e.g. `"sdk"`, `"anonymous"`).
129    Symbolic(&'a str),
130    /// No user attached.
131    None,
132}
133
134impl From<Uuid> for UserIdRef<'_> {
135    fn from(u: Uuid) -> Self {
136        UserIdRef::Uuid(u)
137    }
138}
139impl<'a> From<&'a str> for UserIdRef<'a> {
140    fn from(s: &'a str) -> Self {
141        UserIdRef::Symbolic(s)
142    }
143}
144impl<'a> From<&'a String> for UserIdRef<'a> {
145    fn from(s: &'a String) -> Self {
146        UserIdRef::Symbolic(s.as_str())
147    }
148}
149impl From<Option<Uuid>> for UserIdRef<'_> {
150    fn from(o: Option<Uuid>) -> Self {
151        match o {
152            Some(u) => UserIdRef::Uuid(u),
153            None => UserIdRef::None,
154        }
155    }
156}
157
158/// Format a `tenant_id` for the telemetry wire payload, mirroring
159/// Python `str(user.tenant_id) if user.tenant_id else "Single User Tenant"`.
160///
161/// Lifecycle emitters (pipeline, task, search) thread an
162/// `Option<Uuid>` through the runtime context and call this helper at
163/// the emission site so the on-the-wire string is byte-for-byte
164/// identical to the Python implementation when no tenant has been
165/// configured.
166#[inline]
167pub fn tenant_id_for_telemetry(tenant_id: Option<Uuid>) -> String {
168    match tenant_id {
169        Some(id) => id.to_string(),
170        None => "Single User Tenant".to_string(),
171    }
172}
173
174/// Returns the cognee crate version string for use in analytics
175/// payloads. Matches Python's `cognee.__version__`.
176///
177/// Equivalent to `env!("CARGO_PKG_VERSION")` evaluated inside the
178/// `cognee-telemetry` crate. The workspace pins all cognee crates to
179/// the same version via `version.workspace = true`, so the value is
180/// the same as `cognee-lib`'s reported version. Lifecycle emitters in
181/// `cognee-core` and elsewhere should call this accessor instead of
182/// inlining `env!("CARGO_PKG_VERSION")`, which would otherwise expand
183/// to the calling crate's version.
184///
185/// Always available — does not depend on the `telemetry` feature
186/// flag.
187#[inline]
188pub fn cognee_version() -> &'static str {
189    env!("CARGO_PKG_VERSION")
190}
191
192/// Fire-and-forget product-analytics event.
193///
194/// Returns immediately; the HTTP POST is dispatched on a detached
195/// tokio task with a 5-second (configurable via
196/// `TELEMETRY_REQUEST_TIMEOUT`) total timeout. Errors are swallowed
197/// at debug level on the `cognee.telemetry` tracing target. When
198/// called outside a tokio runtime, falls back to a one-shot
199/// single-thread runtime (locked decision 5) and logs a `warn`-level
200/// notice — behaviour is correct (the event still fires) but
201/// indicates a perf-improvement opportunity.
202///
203/// No-op when:
204/// - the `telemetry` cargo feature is disabled at compile time
205///   (function still exists but compiles to an empty body, so
206///   consuming code stays binary-compatible across feature flips),
207/// - `TELEMETRY_DISABLED` is set to a non-empty value at runtime,
208/// - `ENV` is `"test"` or `"dev"`.
209///
210/// # Environment variables
211///
212/// | Var | Default | Effect |
213/// |---|---|---|
214/// | `TELEMETRY_DISABLED` | unset | Any non-empty value disables. |
215/// | `ENV` | unset | If `test` or `dev`, disables. |
216/// | `LLM_API_KEY` | unset | Hashed into `api_key_tracking_id` (read at every call). |
217/// | `TRACKING_ID` | unset | Override `anonymous_id`. |
218/// | `TELEMETRY_API_KEY_TRACKING_SALT` | (well-known default) | Override PBKDF2 salt. |
219/// | `TELEMETRY_REQUEST_TIMEOUT` | `5` | Total HTTP timeout (seconds), clamped `[1, 60]`. |
220///
221/// See the [user-facing
222/// guide](https://github.com/topoteretes/cognee-rs/blob/main/docs/observability/send_telemetry.md)
223/// for the full reference.
224pub fn send_telemetry<'a>(
225    event_name: &str,
226    user_id: impl Into<UserIdRef<'a>>,
227    additional_properties: Option<PropertyValue>,
228) {
229    let _ = try_send_telemetry(event_name, user_id, additional_properties);
230}
231
232/// Same as [`send_telemetry`] but returns
233/// `Result<(), TelemetryError>` for callers that want to know whether
234/// dispatch was attempted.
235///
236/// The `Ok(())` return does **not** mean the proxy received the
237/// payload — it means the dispatch was scheduled. Transport failures
238/// are still swallowed at debug level on the `cognee.telemetry`
239/// target (mirrors Python's fire-and-forget semantics).
240///
241/// In current builds this function always returns `Ok(())`. The
242/// [`TelemetryError`] variant exists so future failure modes
243/// (e.g. backpressure rejection) can be surfaced without a breaking
244/// change to the signature. Honours the same opt-out and runtime
245/// fallback semantics as [`send_telemetry`]; see that function's
246/// rustdoc for env-var details.
247pub fn try_send_telemetry<'a>(
248    event_name: &str,
249    user_id: impl Into<UserIdRef<'a>>,
250    additional_properties: Option<PropertyValue>,
251) -> Result<(), TelemetryError> {
252    let user_id = user_id.into();
253    #[cfg(feature = "telemetry")]
254    {
255        real::send_telemetry_impl(event_name, user_id, additional_properties);
256    }
257    #[cfg(not(feature = "telemetry"))]
258    {
259        // Drop borrowed/owned args explicitly so unused-variable lints
260        // don't fire when the telemetry feature is off.
261        let _ = (event_name, user_id, additional_properties);
262        noop::send_telemetry_impl();
263    }
264    Ok(())
265}