Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions crates/app/src/app_menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ actions!(
ReloadPlugins,
Quit,
CopyDebugInfo,
ReportIssue,
ReportBug,
RequestFeature,
ReportUxIssue,
OpenLogsFolder,
OpenAbout,
/// What every greyed-out menu item below dispatches. Deliberately unhandled: the label is
Expand Down Expand Up @@ -225,7 +227,15 @@ fn app_menus(cx: &gpui::App) -> Vec<Menu> {
// Unit actions, not `OpenBrowser { url }`: this runs once at startup, while the
// debug info those items carry changes with every project opened and plugin loaded.
MenuItem::action("Copy Debug Info", CopyDebugInfo),
MenuItem::action("Report an Issue", ReportIssue),
MenuItem::submenu(Menu {
name: "Send Feedback".into(),
disabled: false,
items: vec![
MenuItem::action("Report a Bug…", ReportBug),
MenuItem::action("Request a Feature…", RequestFeature),
MenuItem::action("Report a UI/UX Issue…", ReportUxIssue),
],
}),
MenuItem::action("Open Logs Folder", OpenLogsFolder),
MenuItem::Separator,
MenuItem::action(format!("Version {}", env!("CARGO_PKG_VERSION")), OpenAbout),
Expand Down
91 changes: 85 additions & 6 deletions crates/app/src/logging.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The session log, the panic hook, and the debug-info dump behind Help ▸ Copy Debug Info.
//! The session log, panic hook, and reports behind Help ▸ Copy Debug Info and Send Feedback.
//!
//! Everything here shares one thing — the path to the current session's log file — which is why
//! the logger, the panic hook, the log tail, and the dump that embeds it live in one module.
Expand All @@ -7,9 +7,9 @@
//! `qrate.old.log` is the one before it. That is the whole retention policy, and it is what makes
//! "reproduce it, then send me the log" work — the reproduction is always the current file.
//!
//! Nothing is uploaded. `qrate.dvnl.work` is a static site and cannot receive a report, so the
//! dump travels by clipboard or by a prefilled GitHub issue URL, both of which put the user in
//! front of the text before it leaves the machine.
//! Reports open on the qrate site with diagnostics and a short, redacted log tail in the fragment.
//! The fragment is not sent in the page request, and the user reviews what to include before the
//! site uploads anything.

use std::fmt::Write as _;
use std::fs::{self, File};
Expand All @@ -22,6 +22,17 @@ use simplelog::{
ColorChoice, CombinedLogger, ConfigBuilder, LevelFilter, TermLogger, TerminalMode, WriteLogger,
};

pub enum FeedbackKind {
Bug,
Feature,
Ux,
}

#[cfg(debug_assertions)]
const FEEDBACK_URL: &str = "http://localhost:4321/feedback";
#[cfg(not(debug_assertions))]
const FEEDBACK_URL: &str = "https://qrate.dvnl.work/feedback";

/// Where the current session writes. `None` only if the OS has no local data dir, in which case
/// there is nowhere to log and the terminal sink is all there is.
pub fn log_path() -> Option<PathBuf> {
Expand Down Expand Up @@ -195,8 +206,7 @@ pub fn debug_info(cx: &App, log_lines: usize) -> String {
Some(project) => {
let _ = writeln!(
out,
"\nProject: {} ({} rows x {} columns)",
project.file.display(),
"\nProject: open ({} rows x {} columns)",
project.data.rows.len(),
project.data.headers.len()
);
Expand Down Expand Up @@ -240,6 +250,18 @@ fn log_tail(lines: usize) -> String {
all[all.len().saturating_sub(lines)..].join("\n")
}

fn feedback_log_tail() -> String {
let log = redact(&log_tail(30));
if log.chars().count() <= 1_500 {
return log;
}
let tail: String = log.chars().rev().take(1_500).collect();
format!(
"(earlier lines omitted)\n{}",
tail.chars().rev().collect::<String>()
)
}

/// Replace the user's home directory with `~` throughout.
///
/// The dump carries project paths and plugin file names, which on a real machine means a real
Expand Down Expand Up @@ -268,6 +290,42 @@ pub fn urlencode(text: &str) -> String {
})
}

/// Open the hosted form with diagnostics and a bounded log tail in the fragment.
///
/// The page removes the fragment after reading it and shows both values before submission.
pub fn feedback_url(cx: &App, kind: Option<FeedbackKind>) -> String {
let kind = match kind {
Some(FeedbackKind::Bug) => "?type=bug",
Some(FeedbackKind::Feature) => "?type=feature",
Some(FeedbackKind::Ux) => "?type=ui_ux",
None => "",
};
let plugins = plugin_host::status(cx);
let project = cx
.try_global::<settings::project::CurrentProject>()
.map(|project| {
serde_json::json!({
"rows": project.data.rows.len(),
"columns": project.data.headers.len(),
})
});
let diagnostics = serde_json::json!({
"schema": 1,
"version": version(),
"commit": env!("QRATE_GIT_SHA"),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"project": project,
"plugins": plugins.len(),
"plugin_failures": plugins.iter().filter(|(_, error)| error.is_some()).count(),
});
format!(
"{FEEDBACK_URL}{kind}#diagnostics={}&logs={}",
urlencode(&diagnostics.to_string()),
urlencode(&feedback_log_tail())
)
}

/// The path Help ▸ Open Logs Folder reveals, or the folder itself if this session never opened a
/// file to reveal.
pub fn reveal_target() -> Option<PathBuf> {
Expand All @@ -294,6 +352,27 @@ mod tests {
assert_eq!(super::urlencode("qrate-0.1_x.y~z"), "qrate-0.1_x.y~z");
}

#[gpui::test]
fn feedback_links_select_the_form_and_keep_diagnostics_in_the_fragment(
cx: &mut gpui::TestAppContext,
) {
let (bug, feature, ux) = cx.update(|cx| {
(
super::feedback_url(cx, Some(super::FeedbackKind::Bug)),
super::feedback_url(cx, Some(super::FeedbackKind::Feature)),
super::feedback_url(cx, Some(super::FeedbackKind::Ux)),
)
});

assert!(bug.starts_with(&format!("{}?type=bug#diagnostics=", super::FEEDBACK_URL)));
assert!(feature.contains("?type=feature#diagnostics="));
assert!(ux.contains("?type=ui_ux#diagnostics="));
assert!(bug.contains("%22schema%22%3A1"));
assert!(bug.contains("&logs="));
assert!(!bug.contains("CPU"));
assert!(bug.len() < 7_000);
}

/// The demotion has to be narrow: it must catch gpui's two teardown messages and nothing else,
/// or a real failure quietly stops being an error.
#[test]
Expand Down
18 changes: 12 additions & 6 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use crate::{
actions::{NewProject, ToggleBottomDock, ToggleLeftDock, ToggleRightDock},
app_menus::{
CopyDebugInfo, OpenAbout, OpenColumnSettings, OpenLogsFolder, OpenPluginsFolder,
OpenProjects, OpenSettings, Quit, REPO_URL, ReloadPlugins, ReportIssue,
OpenProjects, OpenSettings, Quit, ReloadPlugins, ReportBug, ReportUxIssue, RequestFeature,
},
status_items::build_status_bar_registry,
title_items::build_title_bar_registry,
Expand Down Expand Up @@ -515,11 +515,17 @@ fn main() {
cx.on_action(|_: &CopyDebugInfo, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(logging::debug_info(cx, 200)));
});
cx.on_action(|_: &ReportIssue, cx| {
// A shorter tail than the clipboard gets: GitHub stops honouring `?body=` somewhere
// past 8 KB, and a silently truncated report is worse than a short one.
let body = logging::urlencode(&logging::debug_info(cx, 30));
cx.open_url(&format!("{REPO_URL}/issues/new?body={body}"));
cx.on_action(|_: &ReportBug, cx| {
cx.open_url(&logging::feedback_url(cx, Some(logging::FeedbackKind::Bug)));
});
cx.on_action(|_: &RequestFeature, cx| {
cx.open_url(&logging::feedback_url(
cx,
Some(logging::FeedbackKind::Feature),
));
});
cx.on_action(|_: &ReportUxIssue, cx| {
cx.open_url(&logging::feedback_url(cx, Some(logging::FeedbackKind::Ux)));
});
cx.on_action(|_: &OpenLogsFolder, _| match logging::reveal_target() {
Some(target) => {
Expand Down
27 changes: 23 additions & 4 deletions crates/app/src/title_items/launcher_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use window_wrapper::OpenBrowser;
use crate::actions::NewProject;
use crate::app_menus::{
CopyDebugInfo, OpenAbout, OpenLogsFolder, OpenPluginsFolder, OpenSettings, REPO_URL,
ReloadPlugins, ReportIssue,
ReloadPlugins, ReportBug, ReportUxIssue, RequestFeature,
};
use crate::title_items::update_notice::UpdateNotice;

Expand Down Expand Up @@ -62,7 +62,7 @@ impl Render for LauncherBar {
menu.menu("Plugins Folder", Box::new(OpenPluginsFolder))
.menu("Reload Plugins", Box::new(ReloadPlugins))
})
.submenu("Help", window, cx, |menu, _, _| {
.submenu("Help", window, cx, |menu, window, cx| {
menu.menu(
"Repository",
Box::new(OpenBrowser {
Expand All @@ -77,7 +77,11 @@ impl Render for LauncherBar {
)
.separator()
.menu("Copy Debug Info", Box::new(CopyDebugInfo))
.menu("Report an Issue", Box::new(ReportIssue))
.submenu("Send Feedback", window, cx, |menu, _, _| {
menu.menu("Report a Bug…", Box::new(ReportBug))
.menu("Request a Feature…", Box::new(RequestFeature))
.menu("Report a UI/UX Issue…", Box::new(ReportUxIssue))
})
.menu("Open Logs Folder", Box::new(OpenLogsFolder))
})
.separator()
Expand All @@ -91,6 +95,21 @@ impl Render for LauncherBar {
)
}),
)
.child(self.update.clone())
.child(
h_flex()
.gap_2()
.child(
Button::new("launcher-feedback")
.small()
.compact()
.ghost()
.label("Feedback")
.occlude()
.on_click(|_, _, cx| {
cx.open_url(&crate::logging::feedback_url(cx, None))
}),
)
.child(self.update.clone()),
)
}
}
23 changes: 23 additions & 0 deletions crates/app/src/title_items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ mod update_notice;

use gpui::*;
use gpui_component::{
ActiveTheme,
dock::{DockArea, DockPlacement},
h_flex,
menu::AppMenuBar,
};
use plugin_api::{Bar, Side};
Expand All @@ -14,6 +16,25 @@ use update_notice::UpdateNotice;
use window_wrapper::{BarRegistry, title_bar::TitleBarRegistry};
use workspace::DockToggleButton;

struct FeedbackButton;

impl Render for FeedbackButton {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.id("feedback")
.px(px(6.))
.py(px(2.))
.rounded_md()
.text_sm()
.text_color(cx.theme().muted_foreground)
.cursor_pointer()
.hover(|this| this.bg(cx.theme().secondary_hover))
.occlude()
.child("Feedback")
.on_click(|_, _, cx| cx.open_url(&crate::logging::feedback_url(cx, None)))
}
}

/// Populate the title bar registry. The app menus are the one always-present item, so they
/// are registered on the left by default. Right: generic open/close buttons for each dock.
pub fn build_title_bar_registry(cx: &mut App, dock: WeakEntity<DockArea>) -> TitleBarRegistry {
Expand All @@ -30,6 +51,8 @@ pub fn build_title_bar_registry(cx: &mut App, dock: WeakEntity<DockArea>) -> Tit
let plugins = cx.new(|cx| PluginBar::new(Bar::Title, Side::Right, cx));
registry.items_mut().add_right(plugins);

registry.items_mut().add_right(cx.new(|_| FeedbackButton));

// Download progress and the explicit restart action from the signed updater.
// Text before buttons, on the same reasoning as the plugin bar above.
let update_notice = cx.new(UpdateNotice::new);
Expand Down
Loading