Add telemetry::event! (#22146)

Conrad Irwin created

CC @JosephTLyons

Release Notes:

- N/A

Change summary

Cargo.lock                                      | 16 ++++
Cargo.toml                                      |  2 
crates/client/Cargo.toml                        |  1 
crates/client/src/telemetry.rs                  | 76 ++++--------------
crates/collab/src/api/events.rs                 |  4 +
crates/command_palette/Cargo.toml               |  1 
crates/command_palette/src/command_palette.rs   | 31 ++-----
crates/editor/Cargo.toml                        |  1 
crates/editor/src/editor.rs                     | 13 +-
crates/editor/src/items.rs                      |  4 
crates/extension_host/Cargo.toml                |  1 
crates/extension_host/src/extension_host.rs     | 15 +--
crates/repl/Cargo.toml                          |  1 
crates/repl/src/repl.rs                         |  5 
crates/repl/src/repl_editor.rs                  |  8 -
crates/repl/src/repl_store.rs                   | 13 --
crates/repl/src/session.rs                      | 29 +++---
crates/telemetry/Cargo.toml                     | 18 ++++
crates/telemetry/LICENSE-GPL                    |  1 
crates/telemetry/src/telemetry.rs               | 57 ++++++++++++++
crates/telemetry_events/Cargo.toml              |  1 
crates/telemetry_events/src/telemetry_events.rs |  9 ++
crates/zed/src/main.rs                          |  6 -
crates/zed/src/zed.rs                           |  6 -
24 files changed, 179 insertions(+), 140 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2547,6 +2547,7 @@ dependencies = [
  "settings",
  "sha2",
  "smol",
+ "telemetry",
  "telemetry_events",
  "text",
  "thiserror 1.0.69",
@@ -2841,6 +2842,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "telemetry",
  "theme",
  "ui",
  "util",
@@ -3938,6 +3940,7 @@ dependencies = [
  "snippet",
  "sum_tree",
  "task",
+ "telemetry",
  "tempfile",
  "text",
  "theme",
@@ -4373,6 +4376,7 @@ dependencies = [
  "serde_json_lenient",
  "settings",
  "task",
+ "telemetry",
  "tempfile",
  "theme",
  "theme_extension",
@@ -10399,6 +10403,7 @@ dependencies = [
  "serde_json",
  "settings",
  "smol",
+ "telemetry",
  "terminal",
  "terminal_view",
  "theme",
@@ -12663,12 +12668,23 @@ dependencies = [
  "zed_actions",
 ]
 
+[[package]]
+name = "telemetry"
+version = "0.1.0"
+dependencies = [
+ "futures 0.3.31",
+ "serde",
+ "serde_json",
+ "telemetry_events",
+]
+
 [[package]]
 name = "telemetry_events"
 version = "0.1.0"
 dependencies = [
  "semantic_version",
  "serde",
+ "serde_json",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -117,6 +117,7 @@ members = [
     "crates/tab_switcher",
     "crates/task",
     "crates/tasks_ui",
+    "crates/telemetry",
     "crates/telemetry_events",
     "crates/terminal",
     "crates/terminal_view",
@@ -305,6 +306,7 @@ supermaven_api = { path = "crates/supermaven_api" }
 tab_switcher = { path = "crates/tab_switcher" }
 task = { path = "crates/task" }
 tasks_ui = { path = "crates/tasks_ui" }
+telemetry = { path = "crates/telemetry" }
 telemetry_events = { path = "crates/telemetry_events" }
 terminal = { path = "crates/terminal" }
 terminal_view = { path = "crates/terminal_view" }

crates/client/Cargo.toml 🔗

@@ -51,6 +51,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
 url.workspace = true
 util.workspace = true
 worktree.workspace = true
+telemetry.workspace = true
 
 [dev-dependencies]
 clock = { workspace = true, features = ["test-support"] }

crates/client/src/telemetry.rs 🔗

@@ -4,7 +4,8 @@ use crate::{ChannelId, TelemetrySettings};
 use anyhow::Result;
 use clock::SystemClock;
 use collections::{HashMap, HashSet};
-use futures::Future;
+use futures::channel::mpsc;
+use futures::{Future, StreamExt};
 use gpui::{AppContext, BackgroundExecutor, Task};
 use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
 use once_cell::sync::Lazy;
@@ -17,9 +18,8 @@ use std::io::Write;
 use std::time::Instant;
 use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
 use telemetry_events::{
-    ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
-    EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
-    InlineCompletionRatingEvent, ReplEvent, SettingEvent,
+    AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
+    InlineCompletionEvent, InlineCompletionRating, InlineCompletionRatingEvent, SettingEvent,
 };
 use util::{ResultExt, TryFutureExt};
 use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -245,7 +245,6 @@ impl Telemetry {
         })
         .detach();
 
-        // TODO: Replace all hardware stuff with nested SystemSpecs json
         let this = Arc::new(Self {
             clock,
             http_client: client,
@@ -253,6 +252,21 @@ impl Telemetry {
             state,
         });
 
+        let (tx, mut rx) = mpsc::unbounded();
+        ::telemetry::init(tx);
+
+        cx.background_executor()
+            .spawn({
+                let this = Arc::downgrade(&this);
+                async move {
+                    while let Some(event) = rx.next().await {
+                        let Some(state) = this.upgrade() else { break };
+                        state.report_event(Event::Flexible(event))
+                    }
+                }
+            })
+            .detach();
+
         // We should only ever have one instance of Telemetry, leak the subscription to keep it alive
         // rather than store in TelemetryState, complicating spawn as subscriptions are not Send
         std::mem::forget(cx.on_app_quit({
@@ -320,27 +334,6 @@ impl Telemetry {
         drop(state);
     }
 
-    pub fn report_editor_event(
-        self: &Arc<Self>,
-        file_extension: Option<String>,
-        vim_mode: bool,
-        operation: &'static str,
-        copilot_enabled: bool,
-        copilot_enabled_for_language: bool,
-        is_via_ssh: bool,
-    ) {
-        let event = Event::Editor(EditorEvent {
-            file_extension,
-            vim_mode,
-            operation: operation.into(),
-            copilot_enabled,
-            copilot_enabled_for_language,
-            is_via_ssh,
-        });
-
-        self.report_event(event)
-    }
-
     pub fn report_inline_completion_event(
         self: &Arc<Self>,
         provider: String,
@@ -410,13 +403,6 @@ impl Telemetry {
         self.report_event(event)
     }
 
-    pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
-        self.report_event(Event::Extension(ExtensionEvent {
-            extension_id,
-            version,
-        }))
-    }
-
     pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
         let mut state = self.state.lock();
         let period_data = state.event_coalescer.log_event(environment);
@@ -436,15 +422,6 @@ impl Telemetry {
         }
     }
 
-    pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
-        let event = Event::Action(ActionEvent {
-            source: source.to_string(),
-            action,
-        });
-
-        self.report_event(event)
-    }
-
     pub fn report_discovered_project_events(
         self: &Arc<Self>,
         worktree_id: WorktreeId,
@@ -491,21 +468,6 @@ impl Telemetry {
         }
     }
 
-    pub fn report_repl_event(
-        self: &Arc<Self>,
-        kernel_language: String,
-        kernel_status: String,
-        repl_session_id: String,
-    ) {
-        let event = Event::Repl(ReplEvent {
-            kernel_language,
-            kernel_status,
-            repl_session_id,
-        });
-
-        self.report_event(event)
-    }
-
     fn report_event(self: &Arc<Self>, event: Event) {
         let mut state = self.state.lock();
 

crates/collab/src/api/events.rs 🔗

@@ -610,6 +610,10 @@ fn for_snowflake(
                 "Kernel Status Changed".to_string(),
                 serde_json::to_value(e).unwrap(),
             ),
+            Event::Flexible(e) => (
+                e.event_type.clone(),
+                serde_json::to_value(&e.event_properties).unwrap(),
+            ),
         };
 
         if let serde_json::Value::Object(ref mut map) = event_properties {

crates/command_palette/Cargo.toml 🔗

@@ -25,6 +25,7 @@ settings.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
+telemetry.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 

crates/command_palette/src/command_palette.rs 🔗

@@ -4,7 +4,7 @@ use std::{
     time::Duration,
 };
 
-use client::{parse_zed_link, telemetry::Telemetry};
+use client::parse_zed_link;
 use collections::HashMap;
 use command_palette_hooks::{
     CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
@@ -63,18 +63,12 @@ impl CommandPalette {
         let Some(previous_focus_handle) = cx.focused() else {
             return;
         };
-        let telemetry = workspace.client().telemetry().clone();
         workspace.toggle_modal(cx, move |cx| {
-            CommandPalette::new(previous_focus_handle, telemetry, query, cx)
+            CommandPalette::new(previous_focus_handle, query, cx)
         });
     }
 
-    fn new(
-        previous_focus_handle: FocusHandle,
-        telemetry: Arc<Telemetry>,
-        query: &str,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    fn new(previous_focus_handle: FocusHandle, query: &str, cx: &mut ViewContext<Self>) -> Self {
         let filter = CommandPaletteFilter::try_global(cx);
 
         let commands = cx
@@ -92,12 +86,8 @@ impl CommandPalette {
             })
             .collect();
 
-        let delegate = CommandPaletteDelegate::new(
-            cx.view().downgrade(),
-            commands,
-            telemetry,
-            previous_focus_handle,
-        );
+        let delegate =
+            CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
 
         let picker = cx.new_view(|cx| {
             let picker = Picker::uniform_list(delegate, cx);
@@ -133,7 +123,6 @@ pub struct CommandPaletteDelegate {
     commands: Vec<Command>,
     matches: Vec<StringMatch>,
     selected_ix: usize,
-    telemetry: Arc<Telemetry>,
     previous_focus_handle: FocusHandle,
     updating_matches: Option<(
         Task<()>,
@@ -167,7 +156,6 @@ impl CommandPaletteDelegate {
     fn new(
         command_palette: WeakView<CommandPalette>,
         commands: Vec<Command>,
-        telemetry: Arc<Telemetry>,
         previous_focus_handle: FocusHandle,
     ) -> Self {
         Self {
@@ -176,7 +164,6 @@ impl CommandPaletteDelegate {
             matches: vec![],
             commands,
             selected_ix: 0,
-            telemetry,
             previous_focus_handle,
             updating_matches: None,
         }
@@ -367,9 +354,11 @@ impl PickerDelegate for CommandPaletteDelegate {
         let action_ix = self.matches[self.selected_ix].candidate_id;
         let command = self.commands.swap_remove(action_ix);
 
-        self.telemetry
-            .report_action_event("command palette", command.name.clone());
-
+        telemetry::event!(
+            "Action Invoked",
+            source = "command palette",
+            action = command.name
+        );
         self.matches.clear();
         self.commands.clear();
         HitCounts::update_global(cx, |hit_counts, _cx| {

crates/editor/Cargo.toml 🔗

@@ -72,6 +72,7 @@ smol.workspace = true
 snippet.workspace = true
 sum_tree.workspace = true
 task.workspace = true
+telemetry.workspace = true
 text.workspace = true
 time.workspace = true
 time_format.workspace = true

crates/editor/src/editor.rs 🔗

@@ -1370,7 +1370,7 @@ impl Editor {
             }
         }
 
-        this.report_editor_event("open", None, cx);
+        this.report_editor_event("Editor Opened", None, cx);
         this
     }
 
@@ -12568,7 +12568,7 @@ impl Editor {
 
     fn report_editor_event(
         &self,
-        operation: &'static str,
+        event_type: &'static str,
         file_extension: Option<String>,
         cx: &AppContext,
     ) {
@@ -12605,15 +12605,14 @@ impl Editor {
             .show_inline_completions;
 
         let project = project.read(cx);
-        let telemetry = project.client().telemetry().clone();
-        telemetry.report_editor_event(
+        telemetry::event!(
+            event_type,
             file_extension,
             vim_mode,
-            operation,
             copilot_enabled,
             copilot_enabled_for_language,
-            project.is_via_ssh(),
-        )
+            is_via_ssh = project.is_via_ssh(),
+        );
     }
 
     /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,

crates/editor/src/items.rs 🔗

@@ -733,7 +733,7 @@ impl Item for Editor {
         project: Model<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.report_editor_event("save", None, cx);
+        self.report_editor_event("Editor Saved", None, cx);
         let buffers = self.buffer().clone().read(cx).all_buffers();
         let buffers = buffers
             .into_iter()
@@ -805,7 +805,7 @@ impl Item for Editor {
             .path
             .extension()
             .map(|a| a.to_string_lossy().to_string());
-        self.report_editor_event("save", file_extension, cx);
+        self.report_editor_event("Editor Saved", file_extension, cx);
 
         project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
     }

crates/extension_host/Cargo.toml 🔗

@@ -43,6 +43,7 @@ serde_json.workspace = true
 serde_json_lenient.workspace = true
 settings.workspace = true
 task.workspace = true
+telemetry.workspace = true
 tempfile.workspace = true
 toml.workspace = true
 url.workspace = true

crates/extension_host/src/extension_host.rs 🔗

@@ -1001,14 +1001,13 @@ impl ExtensionStore {
             extensions_to_unload.len() - reload_count
         );
 
-        if let Some(telemetry) = &self.telemetry {
-            for extension_id in &extensions_to_load {
-                if let Some(extension) = new_index.extensions.get(extension_id) {
-                    telemetry.report_extension_event(
-                        extension_id.clone(),
-                        extension.manifest.version.clone(),
-                    );
-                }
+        for extension_id in &extensions_to_load {
+            if let Some(extension) = new_index.extensions.get(extension_id) {
+                telemetry::event!(
+                    "Extension Loaded",
+                    extension_id,
+                    version = extension.manifest.version
+                );
             }
         }
 

crates/repl/Cargo.toml 🔗

@@ -43,6 +43,7 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
+telemetry.workspace = true
 terminal.workspace = true
 terminal_view.workspace = true
 theme.workspace = true

crates/repl/src/repl.rs 🔗

@@ -24,16 +24,15 @@ pub use crate::repl_sessions_ui::{
 };
 use crate::repl_store::ReplStore;
 pub use crate::session::Session;
-use client::telemetry::Telemetry;
 
 pub const KERNEL_DOCS_URL: &str = "https://zed.dev/docs/repl#changing-kernels";
 
-pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
+pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
     set_dispatcher(zed_dispatcher(cx));
     JupyterSettings::register(cx);
     ::editor::init_settings(cx);
     repl_sessions_ui::init(cx);
-    ReplStore::init(fs, telemetry, cx);
+    ReplStore::init(fs, cx);
 }
 
 fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {

crates/repl/src/repl_editor.rs 🔗

@@ -33,7 +33,6 @@ pub fn assign_kernelspec(
     });
 
     let fs = store.read(cx).fs().clone();
-    let telemetry = store.read(cx).telemetry().clone();
 
     if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() {
         // Drop previous session, start new one
@@ -44,8 +43,7 @@ pub fn assign_kernelspec(
         });
     }
 
-    let session = cx
-        .new_view(|cx| Session::new(weak_editor.clone(), fs, telemetry, kernel_specification, cx));
+    let session = cx.new_view(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, cx));
 
     weak_editor
         .update(cx, |_editor, cx| {
@@ -105,15 +103,13 @@ pub fn run(editor: WeakView<Editor>, move_down: bool, cx: &mut WindowContext) ->
             .ok_or_else(|| anyhow::anyhow!("No kernel found for language: {}", language.name()))?;
 
         let fs = store.read(cx).fs().clone();
-        let telemetry = store.read(cx).telemetry().clone();
 
         let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned()
         {
             session
         } else {
             let weak_editor = editor.downgrade();
-            let session = cx
-                .new_view(|cx| Session::new(weak_editor, fs, telemetry, kernel_specification, cx));
+            let session = cx.new_view(|cx| Session::new(weak_editor, fs, kernel_specification, cx));
 
             editor.update(cx, |_editor, cx| {
                 cx.notify();

crates/repl/src/repl_store.rs 🔗

@@ -1,7 +1,6 @@
 use std::sync::Arc;
 
 use anyhow::Result;
-use client::telemetry::Telemetry;
 use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
 use gpui::{
@@ -28,15 +27,14 @@ pub struct ReplStore {
     kernel_specifications: Vec<KernelSpecification>,
     selected_kernel_for_worktree: HashMap<WorktreeId, KernelSpecification>,
     kernel_specifications_for_worktree: HashMap<WorktreeId, Vec<KernelSpecification>>,
-    telemetry: Arc<Telemetry>,
     _subscriptions: Vec<Subscription>,
 }
 
 impl ReplStore {
     const NAMESPACE: &'static str = "repl";
 
-    pub(crate) fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
-        let store = cx.new_model(move |cx| Self::new(fs, telemetry, cx));
+    pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+        let store = cx.new_model(move |cx| Self::new(fs, cx));
 
         store
             .update(cx, |store, cx| store.refresh_kernelspecs(cx))
@@ -49,14 +47,13 @@ impl ReplStore {
         cx.global::<GlobalReplStore>().0.clone()
     }
 
-    pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut ModelContext<Self>) -> Self {
+    pub fn new(fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
         let subscriptions = vec![cx.observe_global::<SettingsStore>(move |this, cx| {
             this.set_enabled(JupyterSettings::enabled(cx), cx);
         })];
 
         let this = Self {
             fs,
-            telemetry,
             enabled: JupyterSettings::enabled(cx),
             sessions: HashMap::default(),
             kernel_specifications: Vec::new(),
@@ -72,10 +69,6 @@ impl ReplStore {
         &self.fs
     }
 
-    pub fn telemetry(&self) -> &Arc<Telemetry> {
-        &self.telemetry
-    }
-
     pub fn is_enabled(&self) -> bool {
         self.enabled
     }

crates/repl/src/session.rs 🔗

@@ -6,7 +6,6 @@ use crate::{
     outputs::{ExecutionStatus, ExecutionView},
     KernelStatus,
 };
-use client::telemetry::Telemetry;
 use collections::{HashMap, HashSet};
 use editor::{
     display_map::{
@@ -37,7 +36,6 @@ pub struct Session {
     pub kernel: Kernel,
     blocks: HashMap<String, EditorBlock>,
     pub kernel_specification: KernelSpecification,
-    telemetry: Arc<Telemetry>,
     _buffer_subscription: Subscription,
 }
 
@@ -194,7 +192,6 @@ impl Session {
     pub fn new(
         editor: WeakView<Editor>,
         fs: Arc<dyn Fs>,
-        telemetry: Arc<Telemetry>,
         kernel_specification: KernelSpecification,
         cx: &mut ViewContext<Self>,
     ) -> Self {
@@ -221,7 +218,6 @@ impl Session {
             blocks: HashMap::default(),
             kernel_specification,
             _buffer_subscription: subscription,
-            telemetry,
         };
 
         session.start_kernel(cx);
@@ -237,10 +233,11 @@ impl Session {
             .and_then(|editor| editor.read(cx).working_directory(cx))
             .unwrap_or_else(temp_dir);
 
-        self.telemetry.report_repl_event(
-            kernel_language.into(),
-            KernelStatus::Starting.to_string(),
-            cx.entity_id().to_string(),
+        telemetry::event!(
+            "Kernel Status Changed",
+            kernel_language,
+            kernel_status = KernelStatus::Starting.to_string(),
+            repl_session_id = cx.entity_id().to_string(),
         );
 
         let session_view = cx.view().clone();
@@ -488,10 +485,11 @@ impl Session {
             JupyterMessageContent::Status(status) => {
                 self.kernel.set_execution_state(&status.execution_state);
 
-                self.telemetry.report_repl_event(
-                    self.kernel_specification.language().into(),
-                    KernelStatus::from(&self.kernel).to_string(),
-                    cx.entity_id().to_string(),
+                telemetry::event!(
+                    "Kernel Status Changed",
+                    kernel_language = self.kernel_specification.language(),
+                    kernel_status = KernelStatus::from(&self.kernel).to_string(),
+                    repl_session_id = cx.entity_id().to_string(),
                 );
 
                 cx.notify();
@@ -540,12 +538,13 @@ impl Session {
         }
 
         let kernel_status = KernelStatus::from(&kernel).to_string();
-        let kernel_language = self.kernel_specification.language().into();
+        let kernel_language = self.kernel_specification.language();
 
-        self.telemetry.report_repl_event(
+        telemetry::event!(
+            "Kernel Status Changed",
             kernel_language,
             kernel_status,
-            cx.entity_id().to_string(),
+            repl_session_id = cx.entity_id().to_string(),
         );
 
         self.kernel = kernel;

crates/telemetry/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "telemetry"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/telemetry.rs"
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
+telemetry_events.workspace = true
+futures.workspace = true

crates/telemetry/src/telemetry.rs 🔗

@@ -0,0 +1,57 @@
+//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information.
+use futures::channel::mpsc;
+pub use serde_json;
+use std::sync::OnceLock;
+pub use telemetry_events::FlexibleEvent as Event;
+
+/// Macro to create telemetry events and send them to the telemetry queue.
+///
+/// By convention, the name should be "Noun Verbed", e.g. "Keymap Changed"
+/// or "Project Diagnostics Opened".
+///
+/// The properties can be any value that implements serde::Serialize.
+///
+/// ```
+/// telemetry::event!("Keymap Changed", version = "1.0.0");
+/// telemetry::event!("Documentation Viewed", url, source = "Extension Upsell");
+/// ```
+#[macro_export]
+macro_rules! event {
+    ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {{
+        let event = $crate::Event {
+            event_type: $name.to_string(),
+            event_properties: std::collections::HashMap::from([
+                $(
+                    (stringify!($key).to_string(),
+                        $crate::serde_json::value::to_value(&$crate::serialize_property!($key $(= $value)?))
+                            .unwrap_or_else(|_| $crate::serde_json::to_value(&()).unwrap())
+                    ),
+                )+
+            ]),
+        };
+        $crate::send_event(event);
+    }};
+}
+
+#[macro_export]
+macro_rules! serialize_property {
+    ($key:ident) => {
+        $key
+    };
+    ($key:ident = $value:expr) => {
+        $value
+    };
+}
+
+pub fn send_event(event: Event) {
+    if let Some(queue) = TELEMETRY_QUEUE.get() {
+        queue.unbounded_send(event).ok();
+        return;
+    }
+}
+
+pub fn init(tx: mpsc::UnboundedSender<Event>) {
+    TELEMETRY_QUEUE.set(tx).ok();
+}
+
+static TELEMETRY_QUEUE: OnceLock<mpsc::UnboundedSender<Event>> = OnceLock::new();

crates/telemetry_events/Cargo.toml 🔗

@@ -14,3 +14,4 @@ path = "src/telemetry_events.rs"
 [dependencies]
 semantic_version.workspace = true
 serde.workspace = true
+serde_json.workspace = true

crates/telemetry_events/src/telemetry_events.rs 🔗

@@ -2,7 +2,7 @@
 
 use semantic_version::SemanticVersion;
 use serde::{Deserialize, Serialize};
-use std::{fmt::Display, sync::Arc, time::Duration};
+use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration};
 
 #[derive(Serialize, Deserialize, Debug, Clone)]
 pub struct EventRequestBody {
@@ -91,6 +91,7 @@ impl Display for AssistantPhase {
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 #[serde(tag = "type")]
 pub enum Event {
+    Flexible(FlexibleEvent),
     Editor(EditorEvent),
     InlineCompletion(InlineCompletionEvent),
     InlineCompletionRating(InlineCompletionRatingEvent),
@@ -106,6 +107,12 @@ pub enum Event {
     Repl(ReplEvent),
 }
 
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct FlexibleEvent {
+    pub event_type: String,
+    pub event_properties: HashMap<String, serde_json::Value>,
+}
+
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct EditorEvent {
     /// The editor operation performed (open, save)

crates/zed/src/main.rs 🔗

@@ -413,11 +413,7 @@ fn main() {
             cx,
         );
         assistant_tools::init(cx);
-        repl::init(
-            app_state.fs.clone(),
-            app_state.client.telemetry().clone(),
-            cx,
-        );
+        repl::init(app_state.fs.clone(), cx);
         extension_host::init(
             extension_host_proxy,
             app_state.fs.clone(),

crates/zed/src/zed.rs 🔗

@@ -3496,11 +3496,7 @@ mod tests {
             );
             let prompt_builder =
                 assistant::init(app_state.fs.clone(), app_state.client.clone(), false, cx);
-            repl::init(
-                app_state.fs.clone(),
-                app_state.client.telemetry().clone(),
-                cx,
-            );
+            repl::init(app_state.fs.clone(), cx);
             repl::notebook::init(cx);
             tasks_ui::init(cx);
             initialize_workspace(app_state.clone(), prompt_builder, cx);