Enable opt-in telemetry
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -92,4 +92,5 @@ typings/
|
||||
out/
|
||||
|
||||
sqlite.db
|
||||
userData/
|
||||
userData/
|
||||
.env.local
|
||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dyad",
|
||||
"version": "0.1.5-beta.3",
|
||||
"version": "0.1.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dyad",
|
||||
"version": "0.1.5-beta.3",
|
||||
"version": "0.1.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.8",
|
||||
@@ -51,6 +51,7 @@
|
||||
"lucide-react": "^0.487.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"openai": "^4.91.1",
|
||||
"posthog-js": "^1.236.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -9515,6 +9516,17 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@@ -16946,6 +16958,36 @@
|
||||
"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": {
|
||||
"version": "1.0.0-alpha.6",
|
||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||
@@ -16972,6 +17014,16 @@
|
||||
"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": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
@@ -20254,6 +20306,12 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"lucide-react": "^0.487.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"openai": "^4.91.1",
|
||||
"posthog-js": "^1.236.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -121,4 +122,4 @@
|
||||
"update-electron-app": "^3.1.1",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/App.css
43
src/App.css
@@ -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;
|
||||
}
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -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;
|
||||
@@ -21,13 +21,14 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NodeSystemInfo } from "@/ipc/ipc_types";
|
||||
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
type NodeInstallStep =
|
||||
| "install"
|
||||
| "waiting-for-continue"
|
||||
| "continue-processing";
|
||||
|
||||
export function SetupBanner() {
|
||||
const { capture } = usePostHog();
|
||||
const navigate = useNavigate();
|
||||
const { isAnyProviderSetup, loading } = useSettings();
|
||||
const [nodeSystemInfo, setNodeSystemInfo] = useState<NodeSystemInfo | null>(
|
||||
@@ -53,6 +54,7 @@ export function SetupBanner() {
|
||||
}, [checkNode]);
|
||||
|
||||
const handleAiSetupClick = () => {
|
||||
capture("setup-flow:ai-provider-setup-click");
|
||||
navigate({
|
||||
to: providerSettingsRoute.id,
|
||||
params: { provider: "google" },
|
||||
@@ -60,11 +62,13 @@ export function SetupBanner() {
|
||||
};
|
||||
|
||||
const handleNodeInstallClick = useCallback(async () => {
|
||||
capture("setup-flow:start-node-install-click");
|
||||
setNodeInstallStep("waiting-for-continue");
|
||||
IpcClient.getInstance().openExternalUrl(nodeSystemInfo!.nodeDownloadUrl);
|
||||
}, [nodeSystemInfo, setNodeInstallStep]);
|
||||
|
||||
const finishNodeInstall = useCallback(async () => {
|
||||
capture("setup-flow:continue-node-install-click");
|
||||
setNodeInstallStep("continue-processing");
|
||||
await IpcClient.getInstance().reloadEnvPath();
|
||||
await checkNode();
|
||||
|
||||
69
src/components/TelemetryBanner.tsx
Normal file
69
src/components/TelemetryBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
src/components/TelemetrySwitch.tsx
Normal file
25
src/components/TelemetrySwitch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { ChatList } from "./ChatList";
|
||||
import { AppList } from "./AppList";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
// Menu items.
|
||||
const items = [
|
||||
@@ -123,6 +124,8 @@ function AppIcons({
|
||||
}: {
|
||||
onHoverChange: (state: HoverState) => void;
|
||||
}) {
|
||||
const { capture } = usePostHog();
|
||||
|
||||
const routerState = useRouterState();
|
||||
const pathname = routerState.location.pathname;
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ import type { Message } from "@/ipc/ipc_types";
|
||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
import { useRunApp } from "@/hooks/useRunApp";
|
||||
import { AutoApproveSwitch } from "../AutoApproveSwitch";
|
||||
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
const { capture } = usePostHog();
|
||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { settings, updateSettings, isAnyProviderSetup } = useSettings();
|
||||
@@ -104,6 +105,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
const currentInput = inputValue;
|
||||
setInputValue("");
|
||||
await streamMessage({ prompt: currentInput, chatId });
|
||||
capture("chat:submit");
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -124,6 +126,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
`Approving proposal for chatId: ${chatId}, messageId: ${messageId}`
|
||||
);
|
||||
setIsApproving(true);
|
||||
capture("chat:approve");
|
||||
try {
|
||||
const result = await IpcClient.getInstance().approveProposal({
|
||||
chatId,
|
||||
@@ -157,6 +160,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
||||
`Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}`
|
||||
);
|
||||
setIsRejecting(true);
|
||||
capture("chat:reject");
|
||||
try {
|
||||
const result = await IpcClient.getInstance().rejectProposal({
|
||||
chatId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSettings } from "@/hooks/useSettings";
|
||||
import { homeChatInputValueAtom } from "@/atoms/chatAtoms"; // Use a different atom for home input
|
||||
import { useAtom } from "jotai";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) {
|
||||
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||
@@ -14,7 +15,6 @@ export function HomeChatInput({ onSubmit }: { onSubmit: () => void }) {
|
||||
const { streamMessage, isStreaming, setIsStreaming } = useStreamChat({
|
||||
hasChatId: false,
|
||||
}); // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
const adjustHeight = () => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
|
||||
@@ -13,6 +13,17 @@ const PROVIDER_TO_ENV_VAR: Record<string, string> = {
|
||||
// Define a type for the environment variables we expect
|
||||
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() {
|
||||
const [settings, setSettingsAtom] = useAtom(userSettingsAtom);
|
||||
const [envVars, setEnvVarsAtom] = useAtom(envVarsAtom);
|
||||
@@ -49,6 +60,23 @@ export function useSettings() {
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
const updatedSettings = await ipcClient.setUserSettings(newSettings);
|
||||
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);
|
||||
return updatedSettings;
|
||||
} catch (error) {
|
||||
|
||||
@@ -89,6 +89,8 @@ export const UserSettingsSchema = z.object({
|
||||
githubUser: GithubUserSchema.optional(),
|
||||
githubAccessToken: SecretSchema.optional(),
|
||||
autoApproveChanges: z.boolean().optional(),
|
||||
telemetryConsent: z.enum(["opted_in", "opted_out", "unset"]).optional(),
|
||||
telemetryUserId: z.string().optional(),
|
||||
// DEPRECATED.
|
||||
runtimeMode: RuntimeModeSchema.optional(),
|
||||
});
|
||||
|
||||
15
src/main.tsx
15
src/main.tsx
@@ -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>
|
||||
);
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { getUserDataPath } from "../paths/paths";
|
||||
import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas";
|
||||
import { safeStorage } from "electron";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// IF YOU NEED TO UPDATE THIS, YOU'RE PROBABLY DOING SOMETHING WRONG!
|
||||
// Need to maintain backwards compatibility!
|
||||
@@ -12,6 +13,8 @@ const DEFAULT_SETTINGS: UserSettings = {
|
||||
provider: "auto",
|
||||
},
|
||||
providerSettings: {},
|
||||
telemetryConsent: "unset",
|
||||
telemetryUserId: uuidv4(),
|
||||
};
|
||||
|
||||
const SETTINGS_FILE = "user-settings.json";
|
||||
|
||||
@@ -11,6 +11,9 @@ import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { HomeChatInput } from "@/components/chat/HomeChatInput";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { PrivacyBanner } from "@/components/TelemetryBanner";
|
||||
|
||||
export default function HomePage() {
|
||||
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
|
||||
const navigate = useNavigate();
|
||||
@@ -21,7 +24,7 @@ export default function HomePage() {
|
||||
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { streamMessage } = useStreamChat({ hasChatId: false });
|
||||
|
||||
const { capture } = usePostHog();
|
||||
// Get the appId from search params
|
||||
const appId = search.appId ? Number(search.appId) : null;
|
||||
|
||||
@@ -50,6 +53,7 @@ export default function HomePage() {
|
||||
setSelectedAppId(result.app.id);
|
||||
setIsPreviewOpen(false);
|
||||
await refreshApps(); // Ensure refreshApps is awaited if it's async
|
||||
capture("home:chat-submit");
|
||||
navigate({ to: "/chat", search: { id: result.chatId } });
|
||||
} catch (error) {
|
||||
console.error("Failed to create chat:", error);
|
||||
@@ -173,6 +177,7 @@ export default function HomePage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<PrivacyBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import ConfirmationDialog from "@/components/ConfirmationDialog";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { showSuccess, showError } from "@/lib/toast";
|
||||
import { AutoApproveSwitch } from "@/components/AutoApproveSwitch";
|
||||
|
||||
import { TelemetrySwitch } from "@/components/TelemetrySwitch";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
export default function SettingsPage() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||
const { settings } = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch app version
|
||||
@@ -103,6 +105,27 @@ export default function SettingsPage() {
|
||||
</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">
|
||||
<ProviderSettingsGrid configuredProviders={[]} />
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,70 @@
|
||||
import { StrictMode } from "react";
|
||||
import { StrictMode, useEffect } from "react";
|
||||
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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<PostHogProvider client={posthogClient}>
|
||||
<App />
|
||||
</PostHogProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user