edit_prediction: Route edit prediction rating through Cloud (#49385)

Marshall Bowers and Tom created

This PR updates the `Edit Prediction Rated` event to be routed through
Cloud instead of the normal telemetry pipeline.

Closes CLO-224.

Release Notes:

- N/A

---------

Co-authored-by: Tom <tom@zed.dev>

Change summary

crates/cloud_api_client/src/cloud_api_client.rs | 28 ++++++++++++
crates/cloud_api_types/src/cloud_api_types.rs   | 10 ++++
crates/edit_prediction/Cargo.toml               |  2 
crates/edit_prediction/src/edit_prediction.rs   | 41 ++++++++++++++----
4 files changed, 70 insertions(+), 11 deletions(-)

Detailed changes

crates/cloud_api_client/src/cloud_api_client.rs 🔗

@@ -206,6 +206,34 @@ impl CloudApiClient {
 
         Ok(())
     }
+
+    pub async fn submit_edit_prediction_feedback(
+        &self,
+        body: SubmitEditPredictionFeedbackBody,
+    ) -> Result<()> {
+        let request = self.build_request(
+            Request::builder().method(Method::POST).uri(
+                self.http_client
+                    .build_zed_cloud_url("/client/feedback/edit_prediction")?
+                    .as_ref(),
+            ),
+            AsyncBody::from(serde_json::to_string(&body)?),
+        )?;
+
+        let mut response = self.http_client.send(request).await?;
+
+        if !response.status().is_success() {
+            let mut body = String::new();
+            response.body_mut().read_to_string(&mut body).await?;
+
+            anyhow::bail!(
+                "Failed to submit edit prediction feedback.\nStatus: {:?}\nBody: {body}",
+                response.status()
+            )
+        }
+
+        Ok(())
+    }
 }
 
 fn build_request(

crates/cloud_api_types/src/cloud_api_types.rs 🔗

@@ -65,3 +65,13 @@ pub struct SubmitAgentThreadFeedbackBody {
     pub rating: String,
     pub thread: serde_json::Value,
 }
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct SubmitEditPredictionFeedbackBody {
+    pub organization_id: Option<OrganizationId>,
+    pub request_id: String,
+    pub rating: String,
+    pub inputs: serde_json::Value,
+    pub output: Option<String>,
+    pub feedback: String,
+}

crates/edit_prediction/Cargo.toml 🔗

@@ -21,6 +21,7 @@ arrayvec.workspace = true
 brotli.workspace = true
 buffer_diff.workspace = true
 client.workspace = true
+cloud_api_types.workspace = true
 cloud_llm_client.workspace = true
 collections.workspace = true
 copilot.workspace = true
@@ -69,7 +70,6 @@ zstd.workspace = true
 
 [dev-dependencies]
 clock = { workspace = true, features = ["test-support"] }
-cloud_api_types.workspace = true
 cloud_llm_client = { workspace = true, features = ["test-support"] }
 ctor.workspace = true
 gpui = { workspace = true, features = ["test-support"] }

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -1,6 +1,7 @@
 use anyhow::Result;
 use arrayvec::ArrayVec;
 use client::{Client, EditPredictionUsage, UserStore};
+use cloud_api_types::SubmitEditPredictionFeedbackBody;
 use cloud_llm_client::predict_edits_v3::{
     PredictEditsV3Request, PredictEditsV3Response, RawCompletionRequest, RawCompletionResponse,
 };
@@ -2256,18 +2257,38 @@ impl EditPredictionStore {
         feedback: String,
         cx: &mut Context<Self>,
     ) {
+        let organization = self.user_store.read(cx).current_organization();
+
         self.rated_predictions.insert(prediction.id.clone());
-        telemetry::event!(
-            "Edit Prediction Rated",
-            request_id = prediction.id.to_string(),
-            rating,
-            inputs = prediction.inputs,
-            output = prediction
+
+        cx.background_spawn({
+            let client = self.client.clone();
+            let prediction_id = prediction.id.to_string();
+            let inputs = serde_json::to_value(&prediction.inputs);
+            let output = prediction
                 .edit_preview
-                .as_unified_diff(prediction.snapshot.file(), &prediction.edits),
-            feedback
-        );
-        self.client.telemetry().flush_events().detach();
+                .as_unified_diff(prediction.snapshot.file(), &prediction.edits);
+            async move {
+                client
+                    .cloud_client()
+                    .submit_edit_prediction_feedback(SubmitEditPredictionFeedbackBody {
+                        organization_id: organization.map(|organization| organization.id.clone()),
+                        request_id: prediction_id,
+                        rating: match rating {
+                            EditPredictionRating::Positive => "positive".to_string(),
+                            EditPredictionRating::Negative => "negative".to_string(),
+                        },
+                        inputs: inputs?,
+                        output,
+                        feedback,
+                    })
+                    .await?;
+
+                anyhow::Ok(())
+            }
+        })
+        .detach_and_log_err(cx);
+
         cx.notify();
     }
 }