1use crate::config::load_config;
2use crate::identity::{detect_agent, detect_agent_version, get_or_create_client_id};
3
4const DEFAULT_TELEMETRY_URL: &str = "https://api.aichub.org/v1";
5
6pub fn is_telemetry_enabled() -> bool {
7 if let Ok(val) = std::env::var("CHUB_TELEMETRY") {
8 if val == "0" || val == "false" {
9 return false;
10 }
11 }
12 load_config().telemetry
13}
14
15pub fn is_feedback_enabled() -> bool {
16 if let Ok(val) = std::env::var("CHUB_FEEDBACK") {
17 if val == "0" || val == "false" {
18 return false;
19 }
20 }
21 load_config().feedback
22}
23
24pub fn get_telemetry_url() -> String {
25 if let Ok(url) = std::env::var("CHUB_TELEMETRY_URL") {
26 return url;
27 }
28 let config = load_config();
29 if config.telemetry_url.is_empty() {
30 DEFAULT_TELEMETRY_URL.to_string()
31 } else {
32 config.telemetry_url
33 }
34}
35
36#[derive(Debug, Clone, serde::Serialize)]
37pub struct FeedbackResult {
38 pub status: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub reason: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub feedback_id: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub code: Option<u16>,
45}
46
47#[derive(Default)]
48pub struct FeedbackOpts {
49 pub comment: Option<String>,
50 pub doc_lang: Option<String>,
51 pub doc_version: Option<String>,
52 pub target_file: Option<String>,
53 pub labels: Option<Vec<String>>,
54 pub agent: Option<String>,
55 pub model: Option<String>,
56 pub cli_version: Option<String>,
57 pub source: Option<String>,
58}
59
60pub async fn send_feedback(
62 entry_id: &str,
63 entry_type: &str,
64 rating: &str,
65 opts: FeedbackOpts,
66) -> FeedbackResult {
67 if !is_feedback_enabled() {
68 return FeedbackResult {
69 status: "skipped".to_string(),
70 reason: Some("feedback_disabled".to_string()),
71 feedback_id: None,
72 code: None,
73 };
74 }
75
76 let client_id = get_or_create_client_id().unwrap_or_default();
77 let telemetry_url = get_telemetry_url();
78 let agent_name = opts.agent.unwrap_or_else(|| detect_agent().to_string());
79 let agent_version = detect_agent_version();
80
81 let body = serde_json::json!({
82 "entry_id": entry_id,
83 "entry_type": entry_type,
84 "rating": rating,
85 "doc_lang": opts.doc_lang,
86 "doc_version": opts.doc_version,
87 "target_file": opts.target_file,
88 "labels": opts.labels,
89 "comment": opts.comment,
90 "agent": {
91 "name": agent_name,
92 "version": agent_version,
93 "model": opts.model,
94 },
95 "cli_version": opts.cli_version,
96 "source": opts.source,
97 });
98
99 let client = match reqwest::Client::builder()
100 .timeout(std::time::Duration::from_secs(3))
101 .build()
102 {
103 Ok(c) => c,
104 Err(_) => {
105 return FeedbackResult {
106 status: "error".to_string(),
107 reason: Some("network".to_string()),
108 feedback_id: None,
109 code: None,
110 }
111 }
112 };
113
114 let send_result = client
115 .post(format!("{}/feedback", telemetry_url))
116 .header("Content-Type", "application/json")
117 .header("X-Client-ID", &client_id)
118 .json(&body)
119 .send()
120 .await;
121
122 match send_result {
123 Ok(res) => {
124 let status_code = res.status();
125 if status_code.is_success() {
126 let data: serde_json::Value = res.json().await.unwrap_or_default();
127 let feedback_id = data
128 .get("feedback_id")
129 .or_else(|| data.get("id"))
130 .and_then(|v: &serde_json::Value| v.as_str())
131 .map(|s: &str| s.to_string());
132 FeedbackResult {
133 status: "sent".to_string(),
134 reason: None,
135 feedback_id,
136 code: None,
137 }
138 } else {
139 FeedbackResult {
140 status: "error".to_string(),
141 reason: None,
142 feedback_id: None,
143 code: Some(status_code.as_u16()),
144 }
145 }
146 }
147 Err(_) => FeedbackResult {
148 status: "error".to_string(),
149 reason: Some("network".to_string()),
150 feedback_id: None,
151 code: None,
152 },
153 }
154}
155
156pub const VALID_LABELS: &[&str] = &[
158 "accurate",
159 "well-structured",
160 "helpful",
161 "good-examples",
162 "outdated",
163 "inaccurate",
164 "incomplete",
165 "wrong-examples",
166 "wrong-version",
167 "poorly-structured",
168];