Enable opt-in telemetry
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -93,3 +93,4 @@ out/
|
|||||||
|
|
||||||
sqlite.db
|
sqlite.db
|
||||||
userData/
|
userData/
|
||||||
|
.env.local
|
||||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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 { 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();
|
||||||
|
|||||||
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";
|
} 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
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 { 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";
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user