#[cfg(feature = "telemetry")]
use serde::Serialize;
#[cfg(feature = "telemetry")]
use serde_json::Value;
#[cfg(feature = "telemetry")]
#[derive(Debug, Serialize)]
pub struct TelemetryPayload<'a> {
pub anonymous_id: &'a str,
pub event_name: &'a str,
pub user_properties: UserProperties<'a>,
pub properties: Properties<'a>,
}
#[cfg(feature = "telemetry")]
#[derive(Debug, Serialize)]
pub struct UserProperties<'a> {
pub user_id: &'a str,
pub persistent_id: &'a str,
pub api_key_tracking_id: &'a str,
pub api_key_hash: &'a str,
}
#[cfg(feature = "telemetry")]
#[derive(Debug, Serialize)]
pub struct Properties<'a> {
pub time: String,
pub user_id: &'a str,
pub anonymous_id: &'a str,
pub persistent_id: &'a str,
pub api_key_tracking_id: &'a str,
pub api_key_hash: &'a str,
pub sdk_runtime: &'static str,
pub cognee_version: &'static str,
#[serde(flatten)]
pub additional: AdditionalProperties,
}
#[cfg(feature = "telemetry")]
#[derive(Debug, Default, Serialize)]
#[serde(transparent)]
pub struct AdditionalProperties {
inner: serde_json::Map<String, Value>,
}
#[cfg(feature = "telemetry")]
impl AdditionalProperties {
pub fn from_value(v: Option<Value>) -> Self {
match v {
Some(Value::Object(map)) => Self { inner: map },
Some(other) => {
tracing::debug!(
target: "cognee.telemetry",
actual_type = std::any::type_name_of_val(&other),
"additional_properties was not an object; dropping"
);
Self::default()
}
None => Self::default(),
}
}
pub fn as_value_mut(&mut self) -> Value {
Value::Object(std::mem::take(&mut self.inner))
}
pub fn replace_with(&mut self, v: Value) {
if let Value::Object(map) = v {
self.inner = map;
}
}
}
#[cfg(feature = "telemetry")]
pub fn format_time_field(now: chrono::DateTime<chrono::Utc>) -> String {
now.format("%m/%d/%Y").to_string()
}
#[cfg(all(test, feature = "telemetry"))]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code — panics are acceptable failures"
)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn time_field_format() {
let when = chrono::DateTime::parse_from_rfc3339("2026-05-06T12:00:00Z")
.expect("rfc3339 fixture")
.with_timezone(&chrono::Utc);
assert_eq!(format_time_field(when), "05/06/2026");
}
#[test]
fn payload_roundtrips_to_python_compatible_json() {
let additional = AdditionalProperties::from_value(Some(json!({
"endpoint": "POST /api/v1/forget",
})));
let payload = TelemetryPayload {
anonymous_id: "a-id",
event_name: "cognee.forget",
user_properties: UserProperties {
user_id: "u-id",
persistent_id: "p-id",
api_key_tracking_id: "ak_deadbeefcafebabe0123456789abcdef",
api_key_hash: "ak_deadbeefcafebabe0123456789abcdef",
},
properties: Properties {
time: "05/06/2026".into(),
user_id: "u-id",
anonymous_id: "a-id",
persistent_id: "p-id",
api_key_tracking_id: "ak_deadbeefcafebabe0123456789abcdef",
api_key_hash: "ak_deadbeefcafebabe0123456789abcdef",
sdk_runtime: "rust",
cognee_version: "0.1.0",
additional,
},
};
let v = serde_json::to_value(&payload).expect("serialize");
assert_eq!(v["anonymous_id"], "a-id");
assert_eq!(v["event_name"], "cognee.forget");
assert_eq!(
v["user_properties"]["api_key_hash"],
v["user_properties"]["api_key_tracking_id"]
);
assert_eq!(v["properties"]["sdk_runtime"], "rust");
assert_eq!(v["properties"]["time"], "05/06/2026");
assert_eq!(v["properties"]["endpoint"], "POST /api/v1/forget");
}
#[test]
fn from_value_drops_non_object() {
let arr = AdditionalProperties::from_value(Some(json!([1, 2, 3])));
let out = serde_json::to_value(&arr).expect("serialize");
assert_eq!(out, json!({}));
let none = AdditionalProperties::from_value(None);
let out = serde_json::to_value(&none).expect("serialize");
assert_eq!(out, json!({}));
}
}