feat(analytics): add optional Aptabase event tracking UI
Integrate Aptabase analytics via a Tauri plugin and add a privacy notice flow plus a settings dialog to control whether anonymous usage events are sent. The app now tracks key user actions while ensuring analytics never blocks core Git operations. - Add analytics tracking helper and Aptabase plugin wiring - Persist analytics consent in localStorage and gate tracking - Introduce notice and settings dialogs, plus new event calls
This commit is contained in:
+92
-1
@@ -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<typeof setInterval> | 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<AnalyticsSettings>) };
|
||||
}
|
||||
} 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; }}
|
||||
/>
|
||||
|
||||
<div class="shell-body">
|
||||
@@ -2908,6 +2984,21 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if analyticsNoticeOpen}
|
||||
<AnalyticsNoticeDialog
|
||||
enabled={analyticsSettings.enabled}
|
||||
onContinue={acceptAnalyticsNotice}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if appSettingsOpen}
|
||||
<AppSettingsDialog
|
||||
analytics={analyticsSettings}
|
||||
onSave={saveAppSettings}
|
||||
onClose={() => { appSettingsOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if linePatchOpen && linePatchFile}
|
||||
<LinePatchDialog
|
||||
file={linePatchFile}
|
||||
|
||||
Reference in New Issue
Block a user