Skip to content

Commit bf30f4c

Browse files
committed
Send feedback with logs, and count anonymous usage
Send feedback (palette, Settings > Help and feedback) files a problem or suggestion as a ticket of the JuCode account, with screenshots and, by default, the end of the engine and daemon logs: listed, previewable, redacted (tokens, keys, authorization, user names in paths) and gzipped. Anonymous usage counts (starts, sessions by backend, requirements, dispatches, feedback, kinds of errors; version and system; a random id per install, no content) are kept by day and sent now and then. On by default, told once at first start, off in Settings. The privacy text covers both.
1 parent ba85e65 commit bf30f4c

19 files changed

Lines changed: 806 additions & 5 deletions

‎src-tauri/src/app_update.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ pub enum DownloadEvent {
4949
/// The JuCode server's mirror of the desktop manifest, on the API the user's
5050
/// config names (regional domains differ).
5151
fn jucode_manifest() -> String {
52+
format!("{}/v1/public/releases/desktop/latest.json", api_base())
53+
}
54+
55+
/// The JuCode API this app talks to without a login (`jucode_api_url` in
56+
/// config.json; regional domains differ).
57+
pub fn api_base() -> String {
5258
let api = std::env::var_os("HOME")
5359
.or_else(|| std::env::var_os("USERPROFILE"))
5460
.map(|home| std::path::PathBuf::from(home).join(".jucode").join("config.json"))
@@ -57,7 +63,7 @@ fn jucode_manifest() -> String {
5763
.and_then(|config| config["jucode_api_url"].as_str().map(str::to_string))
5864
.filter(|api| !api.trim().is_empty())
5965
.unwrap_or_else(|| "https://api.jucode.net".to_string());
60-
format!("{}/v1/public/releases/desktop/latest.json", api.trim_end_matches('/'))
66+
api.trim_end_matches('/').to_string()
6167
}
6268

6369
async fn check_at(app: &AppHandle, manifest: &str) -> Result<Option<Update>, String> {

‎src-tauri/src/lib.rs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,59 @@ fn put_cloud_settings(settings: serde_json::Value) -> Result<serde_json::Value,
807807
jucode_send("PUT", "/v1/oauth/settings", Some(&serde_json::json!({ "settings": settings })))
808808
}
809809

810+
/// The end of the engine's and the daemon's logs, for a bug report (the
811+
/// app shows them, then redacts and gzips them before sending). A missing
812+
/// log is left out.
813+
#[tauri::command(async)]
814+
fn diagnostic_logs() -> Vec<serde_json::Value> {
815+
use std::io::{Seek, SeekFrom};
816+
const TAIL: u64 = 256 * 1024;
817+
let dir = jucode_dir();
818+
[
819+
("jucode.log", dir.join("logs").join("jucode.log")),
820+
("daemon.log", dir.join("daemon").join("daemon.log")),
821+
]
822+
.into_iter()
823+
.filter_map(|(name, path)| {
824+
let mut file = std::fs::File::open(&path).ok()?;
825+
let size = file.metadata().ok()?.len();
826+
file.seek(SeekFrom::Start(size.saturating_sub(TAIL))).ok()?;
827+
let mut bytes = Vec::new();
828+
file.read_to_end(&mut bytes).ok()?;
829+
Some(serde_json::json!({
830+
"name": name,
831+
"size": size,
832+
"text": String::from_utf8_lossy(&bytes),
833+
}))
834+
})
835+
.collect()
836+
}
837+
838+
/// A bug report or suggestion, as a support ticket of the signed-in user.
839+
#[tauri::command(async)]
840+
fn submit_feedback(ticket: serde_json::Value) -> Result<serde_json::Value, String> {
841+
jucode_send("POST", "/v1/oauth/tickets", Some(&ticket))
842+
}
843+
844+
/// Anonymous usage counts (src/lib/telemetry.svelte.ts), with this app's
845+
/// version and platform; no login needed.
846+
#[tauri::command(async)]
847+
fn send_telemetry(app: AppHandle, install: String, days: serde_json::Value) -> Result<(), String> {
848+
let body = serde_json::json!({
849+
"install": install,
850+
"app": "desktop",
851+
"version": app.package_info().version.to_string(),
852+
"os": std::env::consts::OS,
853+
"arch": std::env::consts::ARCH,
854+
"days": days,
855+
});
856+
ureq::post(&format!("{}/v1/public/telemetry", app_update::api_base()))
857+
.timeout(std::time::Duration::from_secs(15))
858+
.send_json(body)
859+
.map(|_| ())
860+
.map_err(|error| error.to_string())
861+
}
862+
810863
/// DeepSeek account balance (https://api.deepseek.com/user/balance), using the
811864
/// API key stored under providers.deepseek in auth.json.
812865
#[tauri::command(async)]
@@ -3118,6 +3171,9 @@ pub fn run() {
31183171
fetch_agent_usage_recent,
31193172
fetch_cloud_settings,
31203173
put_cloud_settings,
3174+
diagnostic_logs,
3175+
submit_feedback,
3176+
send_telemetry,
31213177
fetch_jucode_models,
31223178
fetch_jucode_groups,
31233179
fetch_deepseek_balance,

‎src/lib/ChatPane.svelte‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import RequirementTag from '$lib/requirements/RequirementTag.svelte';
7676
import { statusLabel } from '$lib/requirements/labels';
7777
import { useRequirements } from '$lib/requirements.svelte';
78+
import { telemetry } from '$lib/telemetry.svelte';
7879
7980
// One full conversation (transcript + composer + approvals + pickers) for a
8081
// single session, extracted from the page so several chats can tile side by
@@ -125,6 +126,7 @@
125126
text,
126127
...(chat.sessionId ? { session: chat.sessionId } : project ? { projects: [project.path] } : {})
127128
});
129+
telemetry.track('requirement_create');
128130
toast.success(t('shell.requirement.noted', { id: r.id }), {
129131
action: { label: t('shell.requirement.undo'), run: () => void reqs.remove(r.id).catch(() => {}) }
130132
});

‎src/lib/CommandPalette.svelte‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import ChatCircleTextIcon from 'phosphor-svelte/lib/ChatCircleTextIcon';
23
import { tick } from 'svelte';
34
import MagnifyingGlassIcon from 'phosphor-svelte/lib/MagnifyingGlassIcon';
45
import PlusIcon from 'phosphor-svelte/lib/PlusIcon';
@@ -43,7 +44,8 @@
4344
onToggleTheme,
4445
onSetup,
4546
onHistory,
46-
onShortcuts
47+
onShortcuts,
48+
onFeedback
4749
}: {
4850
chat: ChatState | undefined;
4951
hasProject: boolean;
@@ -66,6 +68,7 @@
6668
onSetup: (view?: 'login') => void;
6769
onHistory: () => void;
6870
onShortcuts: () => void;
71+
onFeedback: () => void;
6972
} = $props();
7073
7174
type Action = {
@@ -121,6 +124,7 @@
121124
run: wrap(() => onOpenPanel(p.key))
122125
})),
123126
{ id: 'settings', label: t('shell.cmd.settings'), keys: shortcutLabel('settings'), icon: SettingsIcon, keywords: t('shell.cmd.settingsKw'), run: wrap(onSettings) },
127+
{ id: 'feedback', label: t('shell.cmd.feedback'), icon: ChatCircleTextIcon, keywords: t('shell.cmd.feedbackKw'), run: wrap(onFeedback) },
124128
{ id: 'setup', label: t('shell.cmd.setup'), hint: t('shell.cmd.setupHint'), icon: WrenchIcon, keywords: t('shell.cmd.setupKw'), run: wrap(() => onSetup()) },
125129
// Debug builds only: VITE_JUCODE_DEBUG=1 when running or building.
126130
...(import.meta.env.VITE_JUCODE_DEBUG === '1'

0 commit comments

Comments
 (0)