diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1048794..8b95876 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1128,7 +1128,7 @@ dependencies = [ "bitflags 2.13.0", "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -2027,6 +2027,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2034,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -2048,6 +2057,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -2526,8 +2541,10 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-aptabase", "tauri-plugin-dialog", "tauri-plugin-updater", + "tokio", ] [[package]] @@ -2895,6 +2912,22 @@ dependencies = [ "webpki-roots 1.0.8", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -4007,6 +4040,23 @@ dependencies = [ "typenum", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -4059,6 +4109,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -4523,12 +4585,49 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -4554,6 +4653,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix 0.31.3", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "osakit" version = "0.3.1" @@ -5342,11 +5457,13 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -5357,6 +5474,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -6618,6 +6736,15 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "sysctl" version = "0.6.0" @@ -6877,6 +7004,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-aptabase" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075ebe083b5f311bdfbf436f8383d316dfb12267be5fd977b57d7701bdb99987" +dependencies = [ + "futures", + "log", + "os_info", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "sys-locale", + "tauri", + "tauri-plugin", + "time", + "tokio", +] + [[package]] name = "tauri-plugin-dialog" version = "2.7.0" @@ -7308,6 +7455,16 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rayon" version = "2.1.0" @@ -7942,6 +8099,12 @@ dependencies = [ "uuid 0.8.2", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -8952,7 +9115,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand 0.8.6", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c5e6a29..ced9dcb 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,8 +17,10 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri = { version = "2", features = [] } tauri-plugin-dialog = "=2.7.0" +tauri-plugin-aptabase = "1.0" keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] } commit_ai = { path = "crates/commit_ai" } +tokio = "1.52.3" [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 261851f..cb1a694 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,4 @@ fn main() { + println!("cargo:rerun-if-env-changed=APTABASE_KEY"); tauri_build::build(); } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c74b012..d2b1ecb 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,7 @@ "core:window:allow-close", "core:window:allow-is-maximized", "core:window:allow-start-dragging", - "dialog:allow-open" + "dialog:allow-open", + "aptabase:allow-track-event" ] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 55bbf8d..aadcb98 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,13 +16,20 @@ use git::{ open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull, push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files, - restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, - stash_pop, stash_push, undo_last_commit, unstage_files, + restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop, + stash_push, undo_last_commit, unstage_files, }; -fn main() { +#[tokio::main] +async fn main() { tauri::Builder::default() .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_aptabase::Builder::new("A-SH-1344793789").with_options( + tauri_plugin_aptabase::InitOptions { + host: Some("https://aptabase.cbsk-tech.de".to_string()), + flush_interval:None + } + ).build()) .manage(SearchCancellationState::default()) .manage(commit_ai::CommitAiEngine::new()) .plugin(tauri_plugin_dialog::init()) diff --git a/src/App.svelte b/src/App.svelte index 34f73ca..c021194 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,6 +6,8 @@ import TitleBar from "./lib/TitleBar.svelte"; import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte"; + import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte"; + import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte"; import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte"; import BlameDialog from "./lib/components/BlameDialog.svelte"; import BranchPanel from "./lib/components/BranchPanel.svelte"; @@ -95,6 +97,7 @@ import type { AiSettings, + AnalyticsSettings, CommitAiPhase, ConflictFile, ExplorerNode, @@ -123,6 +126,7 @@ isAuthError, stripAuthPrefix, } from "./lib/credentials"; + import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics"; type UpdateToastState = "available" | "downloading" | "installed" | "error"; type AppView = "management" | "repository"; @@ -151,6 +155,7 @@ const RECENT_REPOS_KEY = "gitlite.recentRepos.v1"; const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1"; const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; + const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1"; const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1"; const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1"; const COMMIT_PANEL_DEFAULT_HEIGHT = 220; @@ -200,6 +205,9 @@ let commitAiPollTimer: ReturnType | undefined; let aiSettings: AiSettings = defaultAiSettings(); let aiSettingsOpen = false; + let appSettingsOpen = false; + let analyticsNoticeOpen = false; + let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings(); let localModelOptions: LocalModelOption[] = []; let errorMessage = ""; let operation = ""; @@ -329,6 +337,7 @@ // ── Lifecycle ────────────────────────────────────────────────────────────── onMount(() => { + initAnalytics(); loadRepoLists(); autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL); backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL); @@ -554,6 +563,57 @@ startCommitAiPolling(); } + function defaultAnalyticsSettings(): AnalyticsSettings { + return { + enabled: true, + noticeSeen: false, + }; + } + + function loadAnalyticsSettings(): AnalyticsSettings { + try { + const stored = JSON.parse(localStorage.getItem(ANALYTICS_SETTINGS_KEY) ?? "null") as unknown; + if (stored && typeof stored === "object") { + return { ...defaultAnalyticsSettings(), ...(stored as Partial) }; + } + } catch { /* ignore malformed settings */ } + return defaultAnalyticsSettings(); + } + + function persistAnalyticsSettings(next: AnalyticsSettings) { + try { + localStorage.setItem(ANALYTICS_SETTINGS_KEY, JSON.stringify(next)); + } catch { /* ignore storage quota/private-mode errors */ } + } + + function initAnalytics() { + analyticsSettings = loadAnalyticsSettings(); + if (!analyticsSettings.noticeSeen) { + analyticsNoticeOpen = true; + return; + } + trackEvent("app_started"); + } + + function trackEvent(name: string, props?: AnalyticsEventProperties) { + if (!analyticsSettings.enabled) return; + trackAnalyticsEvent(name, props); + } + + function acceptAnalyticsNotice(enabled: boolean) { + analyticsSettings = { enabled, noticeSeen: true }; + persistAnalyticsSettings(analyticsSettings); + analyticsNoticeOpen = false; + trackEvent("app_started", { first_run: 1 }); + } + + function saveAppSettings(next: AnalyticsSettings) { + analyticsSettings = next; + persistAnalyticsSettings(next); + appSettingsOpen = false; + if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1 }); + } + function updateCommitMessage(message: string) { commitMessage = message; if (message !== lastLocalAiGeneratedMessage) { @@ -1210,6 +1270,10 @@ await refreshCommitHistory(activeRepoPath, bundle.commits); await refreshExplorerFiles(activeRepoPath, bundle.files); lastRepoSwitchAt = Date.now(); + trackEvent("repository_opened", { + changed_files: bundle.status.files.length, + has_upstream: bundle.status.upstream ? 1 : 0, + }); void backgroundFetchRepo(activeRepoPath); }); } @@ -1940,6 +2004,7 @@ try { const result = await getFileBlame(activeRepoPath, node.path); blameLines = result.lines; + trackEvent("blame_opened", { lines: result.lines.length }); } catch (error) { blameError = errorToMessage(error); errorMessage = blameError; @@ -2080,10 +2145,12 @@ await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath); + trackEvent("commit_created", { amend: 1 }); }); return; } + const trackedStagedCount = stagedCount; await runOperation("Committing", async () => { applyStatus(await commit(activeRepoPath, message)); commitMessage = ""; @@ -2092,6 +2159,7 @@ await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath); + trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount }); }); } @@ -2270,6 +2338,12 @@ function openCompareSelect() { if (!hasRepository) return; compareSelectOpen = true; + trackEvent("compare_opened"); + } + + function openGlobalSearchDialog() { + globalSearchOpen = true; + trackEvent("global_search_opened"); } async function compareSelectedCommits() { @@ -2337,6 +2411,7 @@ const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId); if (globalSearchId === searchId) { globalSearchResults = results; + trackEvent("global_search_completed", { results: results.length, case_sensitive: caseSensitive ? 1 : 0 }); } } catch (error) { if (globalSearchId === searchId) { @@ -2468,10 +2543,11 @@ onPull={pullRepo} onPush={pushRepo} onRefresh={refreshRepo} - onSearch={() => { globalSearchOpen = true; }} + onSearch={openGlobalSearchDialog} onCompare={openCompareSelect} onOpenInExplorer={openActiveRepoInExplorer} onToggleAutoRefresh={toggleAutoRefresh} + onOpenSettings={() => { appSettingsOpen = true; }} />
@@ -2908,6 +2984,21 @@ /> {/if} +{#if analyticsNoticeOpen} + +{/if} + +{#if appSettingsOpen} + { appSettingsOpen = false; }} + /> +{/if} + {#if linePatchOpen && linePatchFile} header { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + padding-bottom: 10px; + border-bottom: 1px solid var(--color-border-subtle); + } + .settings-section > header svg { + color: var(--color-accent); + flex: 0 0 auto; + } + .settings-section h3 { + margin: 1px 0 0; + color: var(--color-ink); + font-size: 15px; + font-weight: 750; + } + .settings-toggle-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: 10px; + min-width: 0; + padding: 12px; + border: 1px solid var(--color-border-subtle); + border-radius: 8px; + background: #151827; + color: var(--color-ink-muted); + cursor: pointer; + } + .settings-toggle-row input { + width: 16px; + height: 16px; + margin-top: 2px; + accent-color: var(--color-accent); + } + .settings-toggle-row span { + display: grid; + gap: 4px; + min-width: 0; + } + .settings-toggle-row strong { + color: var(--color-ink); + font-size: 13px; + } + .settings-toggle-row small { + color: var(--color-ink-dim); + font-size: 12px; + line-height: 1.45; + } + .analytics-notice-body { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 14px; + padding: 18px 16px 16px; + } + .analytics-notice-icon { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border: 1px solid rgba(65,209,255,0.28); + border-radius: 10px; + color: var(--color-accent); + background: rgba(65,209,255,0.08); + } + .analytics-notice-copy { + display: grid; + gap: 9px; + min-width: 0; + color: var(--color-ink-muted); + font-size: 13px; + line-height: 1.5; + } + .analytics-notice-copy p { + margin: 0; + } + .analytics-notice-toggle { + grid-column: 1 / -1; + } + .analytics-notice-footer { + align-items: center; + } .ai-provider-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/src/lib/TitleBar.svelte b/src/lib/TitleBar.svelte index b3aaa86..ddc61d5 100644 --- a/src/lib/TitleBar.svelte +++ b/src/lib/TitleBar.svelte @@ -2,7 +2,7 @@ import { onDestroy, onMount } from "svelte"; import { getVersion } from "@tauri-apps/api/app"; import { getCurrentWindow } from "@tauri-apps/api/window"; - import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Upload, X } from "@lucide/svelte"; + import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte"; export let branch: string = ""; export let ahead: number = 0; @@ -21,6 +21,7 @@ export let onCompare: () => void = () => {}; export let onOpenInExplorer: () => void = () => {}; export let onToggleAutoRefresh: () => void = () => {}; + export let onOpenSettings: () => void = () => {}; const win = getCurrentWindow(); let isMaximized = false; @@ -198,6 +199,16 @@ /> Auto + +
diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 0000000..088faa0 --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,12 @@ +import { invoke } from "@tauri-apps/api/core"; + +export type AnalyticsEventProperties = Record; + +export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) { + void invoke("plugin:aptabase|track_event", { + name, + props: props && Object.keys(props).length > 0 ? props : undefined, + }).catch(() => { + // Analytics must never interrupt the Git workflow. + }); +} diff --git a/src/lib/components/AnalyticsNoticeDialog.svelte b/src/lib/components/AnalyticsNoticeDialog.svelte new file mode 100644 index 0000000..fee1ade --- /dev/null +++ b/src/lib/components/AnalyticsNoticeDialog.svelte @@ -0,0 +1,57 @@ + + + diff --git a/src/lib/components/AppSettingsDialog.svelte b/src/lib/components/AppSettingsDialog.svelte new file mode 100644 index 0000000..deda5c6 --- /dev/null +++ b/src/lib/components/AppSettingsDialog.svelte @@ -0,0 +1,68 @@ + + + diff --git a/src/lib/components/RepoLoadingOverlay.svelte b/src/lib/components/RepoLoadingOverlay.svelte index 3e4c1d9..9b66fda 100644 --- a/src/lib/components/RepoLoadingOverlay.svelte +++ b/src/lib/components/RepoLoadingOverlay.svelte @@ -43,7 +43,10 @@