Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:42:09.341Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

telemetry_log.rs

626 lines · 21.6 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.