Skip to repository content626 lines · 21.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:50:45.712Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
telemetry_log.rs
1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use time::OffsetDateTime;
5
6use client::telemetry::Telemetry;
7use collections::{HashMap, HashSet};
8use fs::Fs;
9use futures::StreamExt;
10use gpui::{
11 App, Empty, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, ListState,
12 StyleRefinement, Task, TextStyleRefinement, Window, list, prelude::*,
13};
14use language::LanguageRegistry;
15use markdown::{
16 CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle,
17 WrapButtonVisibility,
18};
19use project::Project;
20use settings::Settings;
21use telemetry_events::{Event, EventWrapper};
22use theme_settings::ThemeSettings;
23use ui::{
24 Icon, IconButton, IconName, IconSize, Label, TextSize, Tooltip, WithScrollbar, prelude::*,
25};
26use workspace::{
27 Item, ItemHandle, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
28 notifications::NotificationId,
29};
30
31const MAX_EVENTS: usize = 10_000;
32
33pub fn init(cx: &mut App) {
34 cx.observe_new(
35 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
36 workspace.register_action(
37 |workspace, _: &zed_actions::OpenTelemetryLog, window, cx| {
38 let telemetry_log =
39 cx.new(|cx| TelemetryLogView::new(workspace.project().clone(), window, cx));
40
41 cx.subscribe(&telemetry_log, |workspace, _, event, cx| {
42 let TelemetryLogEvent::ShowToast(toast) = event;
43 workspace.show_toast(toast.clone(), cx);
44 })
45 .detach();
46
47 workspace.add_item_to_active_pane(
48 Box::new(telemetry_log),
49 None,
50 true,
51 window,
52 cx,
53 );
54 },
55 );
56 },
57 )
58 .detach();
59}
60
61pub struct TelemetryLogView {
62 project: Entity<Project>,
63 focus_handle: FocusHandle,
64 events: VecDeque<TelemetryLogEntry>,
65 list_state: ListState,
66 expanded: HashSet<usize>,
67 search_query: String,
68 filtered_indices: Vec<usize>,
69 _subscription: Task<()>,
70}
71
72struct TelemetryLogEntry {
73 received_at: OffsetDateTime,
74 event_type: SharedString,
75 event_properties: HashMap<String, serde_json::Value>,
76 signed_in: bool,
77 collapsed_md: Option<Entity<Markdown>>,
78 expanded_md: Option<Entity<Markdown>>,
79}
80
81impl TelemetryLogEntry {
82 fn props_as_json_object(&self) -> serde_json::Value {
83 serde_json::Value::Object(
84 self.event_properties
85 .iter()
86 .map(|(k, v)| (k.clone(), v.clone()))
87 .collect(),
88 )
89 }
90}
91
92impl TelemetryLogView {
93 pub fn new(project: Entity<Project>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
94 let telemetry = client::Client::global(cx).telemetry().clone();
95 let fs = <dyn Fs>::global(cx);
96
97 let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
98
99 let subscription = cx.spawn(async move |this, cx| {
100 let subscription = telemetry.subscribe_with_history(fs).await;
101
102 this.update(cx, |this, cx| {
103 let historical_events = match subscription.historical_events {
104 Ok(historical) => {
105 if historical.parse_error_count > 0 {
106 this.show_parse_error_toast(historical.parse_error_count, cx);
107 }
108 historical.events
109 }
110 Err(err) => {
111 this.show_read_error_toast(&err, cx);
112 Vec::new()
113 }
114 };
115
116 this.push_events(
117 historical_events
118 .into_iter()
119 .chain(subscription.queued_events),
120 cx,
121 );
122 })
123 .ok();
124
125 let mut live_events = subscription.live_events;
126 while let Some(event_wrapper) = live_events.next().await {
127 let result = this.update(cx, |this, cx| {
128 this.push_event(event_wrapper, cx);
129 });
130 if result.is_err() {
131 break;
132 }
133 }
134 });
135
136 Self {
137 project,
138 focus_handle: cx.focus_handle(),
139 events: VecDeque::with_capacity(MAX_EVENTS),
140 list_state,
141 expanded: HashSet::default(),
142 search_query: String::new(),
143 filtered_indices: Vec::new(),
144 _subscription: subscription,
145 }
146 }
147
148 fn event_wrapper_to_entry(
149 event_wrapper: &EventWrapper,
150 language_registry: &Arc<LanguageRegistry>,
151 cx: &mut App,
152 ) -> TelemetryLogEntry {
153 let (event_type, std_event_properties): (
154 SharedString,
155 std::collections::HashMap<String, serde_json::Value>,
156 ) = match &event_wrapper.event {
157 Event::Flexible(flexible) => (
158 flexible.event_type.clone().into(),
159 flexible.event_properties.clone(),
160 ),
161 };
162
163 let event_properties: HashMap<String, serde_json::Value> =
164 std_event_properties.into_iter().collect();
165
166 let entry = TelemetryLogEntry {
167 received_at: OffsetDateTime::now_utc(),
168 event_type,
169 event_properties,
170 signed_in: event_wrapper.signed_in,
171 collapsed_md: None,
172 expanded_md: None,
173 };
174
175 let collapsed_md = if !entry.event_properties.is_empty() {
176 Some(collapsed_params_md(
177 &entry.props_as_json_object(),
178 language_registry,
179 cx,
180 ))
181 } else {
182 None
183 };
184
185 TelemetryLogEntry {
186 collapsed_md,
187 ..entry
188 }
189 }
190
191 fn push_event(&mut self, event_wrapper: EventWrapper, cx: &mut Context<Self>) {
192 self.push_events(std::iter::once(event_wrapper), cx);
193 }
194
195 fn push_events(
196 &mut self,
197 event_wrappers: impl Iterator<Item = EventWrapper>,
198 cx: &mut Context<Self>,
199 ) {
200 let language_registry = self.project.read(cx).languages().clone();
201
202 for event_wrapper in event_wrappers {
203 let entry = Self::event_wrapper_to_entry(&event_wrapper, &language_registry, cx);
204 self.events.push_back(entry);
205 }
206
207 while self.events.len() > MAX_EVENTS {
208 self.events.pop_front();
209 }
210
211 self.expanded.retain(|&idx| idx < self.events.len());
212
213 self.recompute_filtered_indices();
214 cx.notify();
215 }
216
217 fn entry_matches_filter(&self, entry: &TelemetryLogEntry) -> bool {
218 if self.search_query.is_empty() {
219 return true;
220 }
221
222 let query_lower = self.search_query.to_lowercase();
223
224 if entry.event_type.to_lowercase().contains(&query_lower) {
225 return true;
226 }
227
228 for (key, value) in &entry.event_properties {
229 if key.to_lowercase().contains(&query_lower) {
230 return true;
231 }
232 let value_str = match value {
233 serde_json::Value::String(s) => s.clone(),
234 other => other.to_string(),
235 };
236 if value_str.to_lowercase().contains(&query_lower) {
237 return true;
238 }
239 }
240
241 false
242 }
243
244 fn recompute_filtered_indices(&mut self) {
245 self.filtered_indices.clear();
246 for (idx, entry) in self.events.iter().enumerate() {
247 if self.entry_matches_filter(entry) {
248 self.filtered_indices.push(idx);
249 }
250 }
251 self.list_state.reset(self.filtered_indices.len());
252 }
253
254 pub fn set_search_query(&mut self, query: String, cx: &mut Context<Self>) {
255 self.search_query = query;
256 self.recompute_filtered_indices();
257 cx.notify();
258 }
259
260 fn clear_events(&mut self, cx: &mut Context<Self>) {
261 self.events.clear();
262 self.expanded.clear();
263 self.filtered_indices.clear();
264 self.list_state.reset(0);
265 cx.notify();
266 }
267
268 fn show_read_error_toast(&self, error: &anyhow::Error, cx: &mut Context<Self>) {
269 struct TelemetryLogReadError;
270 cx.emit(TelemetryLogEvent::ShowToast(Toast::new(
271 NotificationId::unique::<TelemetryLogReadError>(),
272 format!("Failed to read telemetry log: {}", error),
273 )));
274 }
275
276 fn show_parse_error_toast(&self, count: usize, cx: &mut Context<Self>) {
277 struct TelemetryLogParseError;
278 let message = if count == 1 {
279 "1 telemetry log entry failed to parse".to_string()
280 } else {
281 format!("{} telemetry log entries failed to parse", count)
282 };
283 cx.emit(TelemetryLogEvent::ShowToast(Toast::new(
284 NotificationId::unique::<TelemetryLogParseError>(),
285 message,
286 )));
287 }
288
289 fn render_entry(
290 &mut self,
291 filtered_index: usize,
292 window: &mut Window,
293 cx: &mut Context<Self>,
294 ) -> AnyElement {
295 let Some(&event_index) = self.filtered_indices.get(filtered_index) else {
296 return Empty.into_any();
297 };
298
299 let Some(entry) = self.events.get(event_index) else {
300 return Empty.into_any();
301 };
302
303 let base_size = TextSize::Editor.rems(cx);
304 let text_style = window.text_style();
305 let theme = cx.theme().clone();
306 let colors = theme.colors();
307 let border_color = colors.border;
308 let element_background = colors.element_background;
309 let selection_background_color = colors.element_selection_background;
310 let syntax = theme.syntax().clone();
311 let expanded = self.expanded.contains(&event_index);
312
313 let local_timezone =
314 time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
315 let timestamp_str = time_format::format_localized_timestamp(
316 entry.received_at,
317 OffsetDateTime::now_utc(),
318 local_timezone,
319 time_format::TimestampFormat::EnhancedAbsolute,
320 );
321
322 let event_type = entry.event_type.clone();
323 let signed_in = entry.signed_in;
324
325 let collapsed_md = entry.collapsed_md.clone();
326
327 let expanded_md =
328 if expanded && entry.expanded_md.is_none() && !entry.event_properties.is_empty() {
329 let language_registry = self.project.read(cx).languages().clone();
330 let md = expanded_params_md(&entry.props_as_json_object(), &language_registry, cx);
331 if let Some(entry_mut) = self.events.get_mut(event_index) {
332 entry_mut.expanded_md = Some(md.clone());
333 }
334 Some(md)
335 } else if expanded {
336 self.events
337 .get(event_index)
338 .and_then(|e| e.expanded_md.clone())
339 } else {
340 None
341 };
342
343 let params_md = if expanded { expanded_md } else { collapsed_md };
344
345 let theme_settings = ThemeSettings::get_global(cx);
346 let buffer_font_family = theme_settings.buffer_font.family.clone();
347
348 v_flex()
349 .id(filtered_index)
350 .group("telemetry-entry")
351 .cursor_pointer()
352 .font_buffer(cx)
353 .w_full()
354 .py_3()
355 .pl_4()
356 .pr_5()
357 .gap_2()
358 .items_start()
359 .text_size(base_size)
360 .border_color(border_color)
361 .border_b_1()
362 .hover(|this| this.bg(element_background.opacity(0.5)))
363 .on_click(cx.listener(move |this, _, _, cx| {
364 if this.expanded.contains(&event_index) {
365 this.expanded.remove(&event_index);
366 } else {
367 this.expanded.insert(event_index);
368 if let Some(filtered_idx) = this
369 .filtered_indices
370 .iter()
371 .position(|&idx| idx == event_index)
372 {
373 this.list_state.scroll_to_reveal_item(filtered_idx);
374 }
375 }
376 cx.notify()
377 }))
378 .child(
379 h_flex()
380 .w_full()
381 .gap_2()
382 .flex_shrink_0()
383 .child(
384 Icon::new(if expanded {
385 IconName::ChevronDown
386 } else {
387 IconName::ChevronRight
388 })
389 .color(Color::Muted)
390 .size(IconSize::Small),
391 )
392 .child(
393 Label::new(timestamp_str)
394 .buffer_font(cx)
395 .color(Color::Muted)
396 .size(LabelSize::Small),
397 )
398 .child(Label::new(event_type).buffer_font(cx).color(Color::Default))
399 .child(div().flex_1())
400 .when(signed_in, |this| {
401 this.child(
402 div()
403 .child(ui::Chip::new("signed in"))
404 .visible_on_hover("telemetry-entry"),
405 )
406 }),
407 )
408 .when_some(params_md, |this, params| {
409 this.child(
410 div().pl_6().w_full().child(
411 MarkdownElement::new(
412 params,
413 MarkdownStyle {
414 base_text_style: text_style,
415 selection_background_color,
416 syntax: syntax.clone(),
417 code_block_overflow_x_scroll: expanded,
418 code_block: StyleRefinement {
419 text: TextStyleRefinement {
420 font_family: Some(buffer_font_family.clone()),
421 font_size: Some((base_size * 0.8).into()),
422 ..Default::default()
423 },
424 ..Default::default()
425 },
426 ..Default::default()
427 },
428 )
429 .code_block_renderer(CodeBlockRenderer::Default {
430 copy_button_visibility: if expanded {
431 CopyButtonVisibility::VisibleOnHover
432 } else {
433 CopyButtonVisibility::Hidden
434 },
435 wrap_button_visibility: WrapButtonVisibility::Hidden,
436 border: false,
437 }),
438 ),
439 )
440 })
441 .into_any()
442 }
443}
444
445fn collapsed_params_md(
446 params: &serde_json::Value,
447 language_registry: &Arc<LanguageRegistry>,
448 cx: &mut App,
449) -> Entity<Markdown> {
450 let params_json = serde_json::to_string(params).unwrap_or_default();
451 let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
452
453 for ch in params_json.chars() {
454 match ch {
455 '{' => spaced_out_json.push_str("{ "),
456 '}' => spaced_out_json.push_str(" }"),
457 ':' => spaced_out_json.push_str(": "),
458 ',' => spaced_out_json.push_str(", "),
459 c => spaced_out_json.push(c),
460 }
461 }
462
463 let params_md = format!("```json\n{}\n```", spaced_out_json);
464 cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
465}
466
467fn expanded_params_md(
468 params: &serde_json::Value,
469 language_registry: &Arc<LanguageRegistry>,
470 cx: &mut App,
471) -> Entity<Markdown> {
472 let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
473 let params_md = format!("```json\n{}\n```", params_json);
474 cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
475}
476
477pub enum TelemetryLogEvent {
478 ShowToast(Toast),
479}
480
481impl EventEmitter<TelemetryLogEvent> for TelemetryLogView {}
482
483impl Item for TelemetryLogView {
484 type Event = TelemetryLogEvent;
485
486 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
487 "Telemetry Log".into()
488 }
489
490 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
491 Some(Icon::new(IconName::Sparkle))
492 }
493}
494
495impl Focusable for TelemetryLogView {
496 fn focus_handle(&self, _cx: &App) -> FocusHandle {
497 self.focus_handle.clone()
498 }
499}
500
501impl Render for TelemetryLogView {
502 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
503 v_flex()
504 .track_focus(&self.focus_handle)
505 .size_full()
506 .bg(cx.theme().colors().editor_background)
507 .child(if self.filtered_indices.is_empty() {
508 h_flex()
509 .size_full()
510 .justify_center()
511 .items_center()
512 .child(if self.events.is_empty() {
513 "No telemetry events recorded yet"
514 } else {
515 "No events match the current filter"
516 })
517 .into_any()
518 } else {
519 div()
520 .size_full()
521 .flex_grow_1()
522 .child(
523 list(self.list_state.clone(), cx.processor(Self::render_entry))
524 .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
525 .size_full(),
526 )
527 .vertical_scrollbar_for(&self.list_state, window, cx)
528 .into_any()
529 })
530 }
531}
532
533pub struct TelemetryLogToolbarItemView {
534 telemetry_log: Option<Entity<TelemetryLogView>>,
535 search_editor: Entity<editor::Editor>,
536}
537
538impl TelemetryLogToolbarItemView {
539 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
540 let search_editor = cx.new(|cx| {
541 let mut editor = editor::Editor::single_line(window, cx);
542 editor.set_placeholder_text("Filter events...", window, cx);
543 editor
544 });
545
546 cx.subscribe(
547 &search_editor,
548 |this, editor, event: &editor::EditorEvent, cx| {
549 if let editor::EditorEvent::BufferEdited { .. } = event {
550 let query = editor.read(cx).text(cx);
551 if let Some(telemetry_log) = &this.telemetry_log {
552 telemetry_log.update(cx, |log, cx| {
553 log.set_search_query(query, cx);
554 });
555 }
556 }
557 },
558 )
559 .detach();
560
561 Self {
562 telemetry_log: None,
563 search_editor,
564 }
565 }
566}
567
568impl Render for TelemetryLogToolbarItemView {
569 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
570 let Some(telemetry_log) = self.telemetry_log.as_ref() else {
571 return Empty.into_any_element();
572 };
573
574 let telemetry_log_clone = telemetry_log.clone();
575 let has_events = !telemetry_log.read(cx).events.is_empty();
576
577 h_flex()
578 .gap_2()
579 .child(div().w(px(200.)).child(self.search_editor.clone()))
580 .child(
581 IconButton::new("clear_events", IconName::Trash)
582 .icon_size(IconSize::Small)
583 .tooltip(Tooltip::text("Clear Events"))
584 .disabled(!has_events)
585 .on_click(cx.listener(move |_this, _, _window, cx| {
586 telemetry_log_clone.update(cx, |log, cx| {
587 log.clear_events(cx);
588 });
589 })),
590 )
591 .child(
592 IconButton::new("open_log_file", IconName::File)
593 .icon_size(IconSize::Small)
594 .tooltip(Tooltip::text("Open Raw Log File"))
595 .on_click(|_, _window, cx| {
596 let path = Telemetry::log_file_path();
597 cx.open_url(&format!("file://{}", path.display()));
598 }),
599 )
600 .into_any()
601 }
602}
603
604impl EventEmitter<ToolbarItemEvent> for TelemetryLogToolbarItemView {}
605
606impl ToolbarItemView for TelemetryLogToolbarItemView {
607 fn set_active_pane_item(
608 &mut self,
609 active_pane_item: Option<&dyn ItemHandle>,
610 _window: &mut Window,
611 cx: &mut Context<Self>,
612 ) -> ToolbarItemLocation {
613 if let Some(item) = active_pane_item
614 && let Some(telemetry_log) = item.downcast::<TelemetryLogView>()
615 {
616 self.telemetry_log = Some(telemetry_log);
617 cx.notify();
618 return ToolbarItemLocation::PrimaryRight;
619 }
620 if self.telemetry_log.take().is_some() {
621 cx.notify();
622 }
623 ToolbarItemLocation::Hidden
624 }
625}
626