Enable opt-in telemetry

This commit is contained in:
Will Chen
2025-04-21 12:49:54 -07:00
parent 497a3b7dac
commit 16d5320485
18 changed files with 299 additions and 81 deletions

1
.gitignore vendored
View File

@@ -93,3 +93,4 @@ out/
sqlite.db sqlite.db
userData/ userData/
.env.local

62
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "dyad", "name": "dyad",
"version": "0.1.5-beta.3", "version": "0.1.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dyad", "name": "dyad",
"version": "0.1.5-beta.3", "version": "0.1.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^1.2.8", "@ai-sdk/anthropic": "^1.2.8",
@@ -51,6 +51,7 @@
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"openai": "^4.91.1", "openai": "^4.91.1",
"posthog-js": "^1.236.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -9515,6 +9516,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.41.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz",
"integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/crc-32": { "node_modules/crc-32": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
@@ -16946,6 +16958,36 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/posthog-js": {
"version": "1.236.3",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.236.3.tgz",
"integrity": "sha512-pu/km63Ad930buL01cBBtYNP7IiJZphqnlAvuV9kaeawnaVqFd3BdxtoauYDmhCrvkwuFxrSwhtYIVD5Cnr9IQ==",
"license": "MIT",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.4"
},
"peerDependencies": {
"@rrweb/types": "2.0.0-alpha.17",
"rrweb-snapshot": "2.0.0-alpha.17"
},
"peerDependenciesMeta": {
"@rrweb/types": {
"optional": true
},
"rrweb-snapshot": {
"optional": true
}
}
},
"node_modules/posthog-js/node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/postject": { "node_modules/postject": {
"version": "1.0.0-alpha.6", "version": "1.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
@@ -16972,6 +17014,16 @@
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
}, },
"node_modules/preact": {
"version": "10.26.5",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.5.tgz",
"integrity": "sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prebuild-install": { "node_modules/prebuild-install": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -20254,6 +20306,12 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -105,6 +105,7 @@
"lucide-react": "^0.487.0", "lucide-react": "^0.487.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"openai": "^4.91.1", "openai": "^4.91.1",
"posthog-js": "^1.236.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",

View File

@@ -1,43 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,11 +0,0 @@
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";
// The router is automatically initialized by RouterProvider
// so we don't need to call initialize() manually
function App() {
return <RouterProvider router={router} />;
}
export default App;

View File

@@ -21,13 +21,14 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { NodeSystemInfo } from "@/ipc/ipc_types"; import { NodeSystemInfo } from "@/ipc/ipc_types";
import { usePostHog } from "posthog-js/react";
type NodeInstallStep = type NodeInstallStep =
| "install" | "install"
| "waiting-for-continue" | "waiting-for-continue"
| "continue-processing"; | "continue-processing";
export function SetupBanner() { export function SetupBanner() {
const { capture } = usePostHog();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAnyProviderSetup, loading } = useSettings(); const { isAnyProviderSetup, loading } = useSettings();
const [nodeSystemInfo, setNodeSystemInfo] = useState<NodeSystemInfo | null>( const [nodeSystemInfo, setNodeSystemInfo] = useState<NodeSystemInfo | null>(
@@ -53,6 +54,7 @@ export function SetupBanner() {
}, [checkNode]); }, [checkNode]);
const handleAiSetupClick = () => { const handleAiSetupClick = () => {
capture("setup-flow:ai-provider-setup-click");
navigate({ navigate({
to: providerSettingsRoute.id, to: providerSettingsRoute.id,
params: { provider: "google" }, params: { provider: "google" },
@@ -60,11 +62,13 @@ export function SetupBanner() {
}; };
const handleNodeInstallClick = useCallback(async () => { const handleNodeInstallClick = useCallback(async () => {
capture("setup-flow:start-node-install-click");
setNodeInstallStep("waiting-for-continue"); setNodeInstallStep("waiting-for-continue");
IpcClient.getInstance().openExternalUrl(nodeSystemInfo!.nodeDownloadUrl); IpcClient.getInstance().openExternalUrl(nodeSystemInfo!.nodeDownloadUrl);
}, [nodeSystemInfo, setNodeInstallStep]); }, [nodeSystemInfo, setNodeInstallStep]);
const finishNodeInstall = useCallback(async () => { const finishNodeInstall = useCallback(async () => {
capture("setup-flow:continue-node-install-click");
setNodeInstallStep("continue-processing"); setNodeInstallStep("continue-processing");
await IpcClient.getInstance().reloadEnvPath(); await IpcClient.getInstance().reloadEnvPath();
await checkNode(); await checkNode();

View File

@@ -0,0 +1,69 @@
import { IpcClient } from "@/ipc/ipc_client";
import React, { useState } from "react";
import { Button } from "./ui/button";
import { atom, useAtom } from "jotai";
import { useSettings } from "@/hooks/useSettings";
const hideBannerAtom = atom(false);
export function PrivacyBanner() {
const [hideBanner, setHideBanner] = useAtom(hideBannerAtom);
const { settings, updateSettings } = useSettings();
// TODO: Implement state management for banner visibility and user choice
// TODO: Implement functionality for Accept, Reject, Ask me later buttons
// TODO: Add state to hide/show banner based on user choice
if (hideBanner) {
return null;
}
if (settings?.telemetryConsent !== "unset") {
return null;
}
return (
<div className="fixed bg-(--background)/90 bottom-4 right-4 backdrop-blur-md border border-gray-200 dark:border-gray-700 p-4 rounded-lg shadow-lg z-50 max-w-md">
<div className="flex flex-col gap-3">
<div>
<h4 className="text-base font-semibold text-gray-800 dark:text-gray-200">
Share anonymous data?
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Help improve Dyad with anonymous usage data.
<em className="block italic mt-0.5">
Note: this does not log your code or messages.
</em>
<a
onClick={() => {
IpcClient.getInstance().openExternalUrl(
"https://dyad.sh/docs/telemetry"
);
}}
className="cursor-pointer text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
Learn more
</a>
</p>
</div>
<div className="flex gap-2 justify-end">
<Button
variant="default"
onClick={() => {
updateSettings({ telemetryConsent: "opted_in" });
}}
>
Accept
</Button>
<Button
variant="secondary"
onClick={() => {
updateSettings({ telemetryConsent: "opted_out" });
}}
>
Reject
</Button>
<Button variant="ghost" onClick={() => setHideBanner(true)}>
Later
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { useSettings } from "@/hooks/useSettings";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { showInfo } from "@/lib/toast";
export function TelemetrySwitch() {
const { settings, updateSettings } = useSettings();
return (
<div className="flex items-center space-x-2">
<Switch
id="telemetry-switch"
checked={settings?.telemetryConsent === "opted_in"}
onCheckedChange={() => {
updateSettings({
telemetryConsent:
settings?.telemetryConsent === "opted_in"
? "opted_out"
: "opted_in",
});
}}
/>
<Label htmlFor="telemetry-switch">Telemetry</Label>
</div>
);
}

View File

@@ -17,6 +17,7 @@ import {
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { ChatList } from "./ChatList"; import { ChatList } from "./ChatList";
import { AppList } from "./AppList"; import { AppList } from "./AppList";
import { usePostHog } from "posthog-js/react";
// Menu items. // Menu items.
const items = [ const items = [
@@ -123,6 +124,8 @@ function AppIcons({
}: { }: {
onHoverChange: (state: HoverState) => void; onHoverChange: (state: HoverState) => void;
}) { }) {
const { capture } = usePostHog();
const routerState = useRouterState(); const routerState = useRouterState();
const pathname = routerState.location.pathname; const pathname = routerState.location.pathname;

View File

@@ -36,8 +36,9 @@ import type { Message } from "@/ipc/ipc_types";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useRunApp } from "@/hooks/useRunApp"; import { useRunApp } from "@/hooks/useRunApp";
import { AutoApproveSwitch } from "../AutoApproveSwitch"; import { AutoApproveSwitch } from "../AutoApproveSwitch";
import { usePostHog } from "posthog-js/react";
export function ChatInput({ chatId }: { chatId?: number }) { export function ChatInput({ chatId }: { chatId?: number }) {
const { capture } = usePostHog();
const [inputValue, setInputValue] = useAtom(chatInputValueAtom); const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const { settings, updateSettings, isAnyProviderSetup } = useSettings(); const { settings, updateSettings, isAnyProviderSetup } = useSettings();
@@ -104,6 +105,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const currentInput = inputValue; const currentInput = inputValue;
setInputValue(""); setInputValue("");
await streamMessage({ prompt: currentInput, chatId }); await streamMessage({ prompt: currentInput, chatId });
capture("chat:submit");
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -124,6 +126,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
`Approving proposal for chatId: ${chatId}, messageId: ${messageId}` `Approving proposal for chatId: ${chatId}, messageId: ${messageId}`
); );
setIsApproving(true); setIsApproving(true);
capture("chat:approve");
try { try {
const result = await IpcClient.getInstance().approveProposal({ const result = await IpcClient.getInstance().approveProposal({
chatId, chatId,
@@ -157,6 +160,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
`Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}` `Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}`
); );
setIsRejecting(true); setIsRejecting(true);
capture("chat:reject");
try { try {
const result = await IpcClient.getInstance().rejectProposal({ const result = await IpcClient.getInstance().rejectProposal({
chatId, chatId,

View File

@@ -6,6 +6,7 @@ import { useSettings } from "@/hooks/useSettings";
import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { usePostHog } from "posthog-js/react";
export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) { export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) {
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom); const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
@@ -14,7 +15,6 @@ export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) {
const { streamMessage, isStreaming, setIsStreaming } = useStreamChat({ const { streamMessage, isStreaming, setIsStreaming } = useStreamChat({
hasChatId: false, hasChatId: false,
}); // eslint-disable-line @typescript-eslint/no-unused-vars }); // eslint-disable-line @typescript-eslint/no-unused-vars
const adjustHeight = () => { const adjustHeight = () => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
if (textarea) { if (textarea) {

View File

@@ -13,6 +13,17 @@ const PROVIDER_TO_ENV_VAR: Record<string, string> = {
// Define a type for the environment variables we expect // Define a type for the environment variables we expect
type EnvVars = Record<string, string | undefined>; type EnvVars = Record<string, string | undefined>;
const TELEMETRY_CONSENT_KEY = "dyadTelemetryConsent";
const TELEMETRY_USER_ID_KEY = "dyadTelemetryUserId";
export function isTelemetryOptedIn() {
return window.localStorage.getItem(TELEMETRY_CONSENT_KEY) === "opted_in";
}
export function getTelemetryUserId(): string | null {
return window.localStorage.getItem(TELEMETRY_USER_ID_KEY);
}
export function useSettings() { export function useSettings() {
const [settings, setSettingsAtom] = useAtom(userSettingsAtom); const [settings, setSettingsAtom] = useAtom(userSettingsAtom);
const [envVars, setEnvVarsAtom] = useAtom(envVarsAtom); const [envVars, setEnvVarsAtom] = useAtom(envVarsAtom);
@@ -49,6 +60,23 @@ export function useSettings() {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
const updatedSettings = await ipcClient.setUserSettings(newSettings); const updatedSettings = await ipcClient.setUserSettings(newSettings);
setSettingsAtom(updatedSettings); setSettingsAtom(updatedSettings);
if (updatedSettings.telemetryConsent) {
window.localStorage.setItem(
TELEMETRY_CONSENT_KEY,
updatedSettings.telemetryConsent
);
} else {
window.localStorage.removeItem(TELEMETRY_CONSENT_KEY);
}
if (updatedSettings.telemetryUserId) {
window.localStorage.setItem(
TELEMETRY_USER_ID_KEY,
updatedSettings.telemetryUserId
);
} else {
window.localStorage.removeItem(TELEMETRY_USER_ID_KEY);
}
setError(null); setError(null);
return updatedSettings; return updatedSettings;
} catch (error) { } catch (error) {

View File

@@ -89,6 +89,8 @@ export const UserSettingsSchema = z.object({
githubUser: GithubUserSchema.optional(), githubUser: GithubUserSchema.optional(),
githubAccessToken: SecretSchema.optional(), githubAccessToken: SecretSchema.optional(),
autoApproveChanges: z.boolean().optional(), autoApproveChanges: z.boolean().optional(),
telemetryConsent: z.enum(["opted_in", "opted_out", "unset"]).optional(),
telemetryUserId: z.string().optional(),
// DEPRECATED. // DEPRECATED.
runtimeMode: RuntimeModeSchema.optional(), runtimeMode: RuntimeModeSchema.optional(),
}); });

View File

@@ -1,15 +0,0 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { Toaster } from "sonner";
import "./styles/globals.css";
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<TooltipProvider>
<RouterProvider router={router} />
</TooltipProvider>
</React.StrictMode>
);

View File

@@ -3,6 +3,7 @@ import path from "node:path";
import { getUserDataPath } from "../paths/paths"; import { getUserDataPath } from "../paths/paths";
import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas"; import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas";
import { safeStorage } from "electron"; import { safeStorage } from "electron";
import { v4 as uuidv4 } from "uuid";
// IF YOU NEED TO UPDATE THIS, YOU'RE PROBABLY DOING SOMETHING WRONG! // IF YOU NEED TO UPDATE THIS, YOU'RE PROBABLY DOING SOMETHING WRONG!
// Need to maintain backwards compatibility! // Need to maintain backwards compatibility!
@@ -12,6 +13,8 @@ const DEFAULT_SETTINGS: UserSettings = {
provider: "auto", provider: "auto",
}, },
providerSettings: {}, providerSettings: {},
telemetryConsent: "unset",
telemetryUserId: uuidv4(),
}; };
const SETTINGS_FILE = "user-settings.json"; const SETTINGS_FILE = "user-settings.json";

View File

@@ -11,6 +11,9 @@ import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { HomeChatInput } from "@/components/chat/HomeChatInput"; import { HomeChatInput } from "@/components/chat/HomeChatInput";
import { usePostHog } from "posthog-js/react";
import { PrivacyBanner } from "@/components/TelemetryBanner";
export default function HomePage() { export default function HomePage() {
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom); const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -21,7 +24,7 @@ export default function HomePage() {
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { streamMessage } = useStreamChat({ hasChatId: false }); const { streamMessage } = useStreamChat({ hasChatId: false });
const { capture } = usePostHog();
// Get the appId from search params // Get the appId from search params
const appId = search.appId ? Number(search.appId) : null; const appId = search.appId ? Number(search.appId) : null;
@@ -50,6 +53,7 @@ export default function HomePage() {
setSelectedAppId(result.app.id); setSelectedAppId(result.app.id);
setIsPreviewOpen(false); setIsPreviewOpen(false);
await refreshApps(); // Ensure refreshApps is awaited if it's async await refreshApps(); // Ensure refreshApps is awaited if it's async
capture("home:chat-submit");
navigate({ to: "/chat", search: { id: result.chatId } }); navigate({ to: "/chat", search: { id: result.chatId } });
} catch (error) { } catch (error) {
console.error("Failed to create chat:", error); console.error("Failed to create chat:", error);
@@ -173,6 +177,7 @@ export default function HomePage() {
))} ))}
</div> </div>
</div> </div>
<PrivacyBanner />
</div> </div>
); );
} }

View File

@@ -5,12 +5,14 @@ import ConfirmationDialog from "@/components/ConfirmationDialog";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { showSuccess, showError } from "@/lib/toast"; import { showSuccess, showError } from "@/lib/toast";
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch"; import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
import { useSettings } from "@/hooks/useSettings";
export default function SettingsPage() { export default function SettingsPage() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
const [appVersion, setAppVersion] = useState<string | null>(null); const [appVersion, setAppVersion] = useState<string | null>(null);
const { settings } = useSettings();
useEffect(() => { useEffect(() => {
// Fetch app version // Fetch app version
@@ -103,6 +105,27 @@ export default function SettingsPage() {
</div> </div>
</div> </div>
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Telemetry
</h2>
<div className="space-y-2">
<TelemetrySwitch />
<div className="text-sm text-gray-500 dark:text-gray-400">
This records anonymous usage data to improve the product.
</div>
</div>
<div className="mt-2 flex items-center text-sm text-gray-500 dark:text-gray-400">
<span className="mr-2 font-medium">Telemetry ID:</span>
<span className="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono">
{settings ? settings.telemetryUserId : "n/a"}
</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
<ProviderSettingsGrid configuredProviders={[]} /> <ProviderSettingsGrid configuredProviders={[]} />
</div> </div>

View File

@@ -1,9 +1,70 @@
import { StrictMode } from "react"; import { StrictMode, useEffect } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./App"; import { router } from "./router";
import { RouterProvider } from "@tanstack/react-router";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
import { getTelemetryUserId, isTelemetryOptedIn } from "./hooks/useSettings";
const posthogClient = posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
api_host: "https://us.i.posthog.com",
debug: import.meta.env.MODE === "development",
autocapture: false,
capture_pageview: false,
before_send: (event) => {
if (!isTelemetryOptedIn()) {
console.debug("Telemetry not opted in, skipping event", event);
return null;
}
const telemetryUserId = getTelemetryUserId();
if (telemetryUserId) {
posthogClient.identify(telemetryUserId);
}
if (event?.properties["$ip"]) {
event.properties["$ip"] = null;
}
console.debug(
"Telemetry opted in - UUID:",
telemetryUserId,
"sending event",
event
);
return event;
},
persistence: "localStorage",
});
function App() {
useEffect(() => {
// Subscribe to navigation state changes
const unsubscribe = router.subscribe("onResolved", (navigation) => {
// Capture the navigation event in PostHog
posthog.capture("navigation", {
toPath: navigation.toLocation.pathname,
fromPath: navigation.fromLocation?.pathname,
});
// Optionally capture as a standard pageview as well
posthog.capture("$pageview", {
path: navigation.toLocation.pathname,
});
});
// Clean up subscription when component unmounts
return () => {
unsubscribe();
};
}, []);
return <RouterProvider router={router} />;
}
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<PostHogProvider client={posthogClient}>
<App /> <App />
</PostHogProvider>
</StrictMode> </StrictMode>
); );