mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: Analytics with PostHog (#4655)
This commit is contained in:
parent
387c8f1df3
commit
0595d2336a
41
frontend/package-lock.json
generated
41
frontend/package-lock.json
generated
@ -26,6 +26,7 @@
|
||||
"isbot": "^5.1.17",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"posthog-js": "^1.176.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
@ -7864,6 +7865,16 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.38.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz",
|
||||
"integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
@ -9666,6 +9677,11 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -19653,6 +19669,31 @@
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.176.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.176.0.tgz",
|
||||
"integrity": "sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js/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=="
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.24.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"isbot": "^5.1.17",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"posthog-js": "^1.176.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
|
||||
42
frontend/src/components/analytics-consent-form-modal.tsx
Normal file
42
frontend/src/components/analytics-consent-form-modal.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { ModalBackdrop } from "./modals/modal-backdrop";
|
||||
import ModalBody from "./modals/ModalBody";
|
||||
import ModalButton from "./buttons/ModalButton";
|
||||
import {
|
||||
BaseModalTitle,
|
||||
BaseModalDescription,
|
||||
} from "./modals/confirmation-modals/BaseModal";
|
||||
|
||||
export function AnalyticsConsentFormModal() {
|
||||
const fetcher = useFetcher({ key: "set-consent" });
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<fetcher.Form
|
||||
method="POST"
|
||||
action="/set-consent"
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<ModalBody>
|
||||
<BaseModalTitle title="Your Privacy Preferences" />
|
||||
<BaseModalDescription>
|
||||
We use tools to understand how our application is used to improve
|
||||
your experience. You can enable or disable analytics. Your
|
||||
preferences will be stored and can be updated anytime.
|
||||
</BaseModalDescription>
|
||||
|
||||
<label className="flex gap-2 items-center self-start">
|
||||
<input name="analytics" type="checkbox" defaultChecked />
|
||||
Send anonymous usage data
|
||||
</label>
|
||||
|
||||
<ModalButton
|
||||
type="submit"
|
||||
text="Confirm Preferences"
|
||||
className="bg-primary text-white w-full hover:opacity-80"
|
||||
/>
|
||||
</ModalBody>
|
||||
</fetcher.Form>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@ -14,12 +14,14 @@ interface AccountSettingsModalProps {
|
||||
onClose: () => void;
|
||||
selectedLanguage: string;
|
||||
gitHubError: boolean;
|
||||
analyticsConsent: string | null;
|
||||
}
|
||||
|
||||
function AccountSettingsModal({
|
||||
onClose,
|
||||
selectedLanguage,
|
||||
gitHubError,
|
||||
analyticsConsent,
|
||||
}: AccountSettingsModalProps) {
|
||||
const data = useRouteLoaderData<typeof clientLoader>("routes/_oh");
|
||||
const settingsFetcher = useFetcher<typeof settingsClientAction>({
|
||||
@ -32,6 +34,7 @@ function AccountSettingsModal({
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const language = formData.get("language")?.toString();
|
||||
const ghToken = formData.get("ghToken")?.toString();
|
||||
const analytics = formData.get("analytics")?.toString() === "on";
|
||||
|
||||
const accountForm = new FormData();
|
||||
const loginForm = new FormData();
|
||||
@ -44,6 +47,7 @@ function AccountSettingsModal({
|
||||
accountForm.append("language", languageKey ?? "en");
|
||||
}
|
||||
if (ghToken) loginForm.append("ghToken", ghToken);
|
||||
accountForm.append("analytics", analytics.toString());
|
||||
|
||||
settingsFetcher.submit(accountForm, {
|
||||
method: "POST",
|
||||
@ -101,6 +105,15 @@ function AccountSettingsModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex gap-2 items-center self-start">
|
||||
<input
|
||||
name="analytics"
|
||||
type="checkbox"
|
||||
defaultChecked={analyticsConsent === "true"}
|
||||
/>
|
||||
Enable analytics
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<ModalButton
|
||||
disabled={
|
||||
|
||||
@ -21,13 +21,17 @@ export function BaseModalTitle({ title }: BaseModalTitleProps) {
|
||||
}
|
||||
|
||||
interface BaseModalDescriptionProps {
|
||||
description: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function BaseModalDescription({
|
||||
description,
|
||||
children,
|
||||
}: BaseModalDescriptionProps) {
|
||||
return <span className="text-xs text-[#A3A3A3]">{description}</span>;
|
||||
return (
|
||||
<span className="text-xs text-[#A3A3A3]">{children || description}</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface BaseModalProps {
|
||||
|
||||
@ -6,13 +6,25 @@
|
||||
*/
|
||||
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import React, { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { SocketProvider } from "./context/socket";
|
||||
import "./i18n";
|
||||
import store from "./store";
|
||||
|
||||
function PosthogInit() {
|
||||
React.useEffect(() => {
|
||||
posthog.init("phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA", {
|
||||
api_host: "https://us.i.posthog.com",
|
||||
person_profiles: "identified_only",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareApp() {
|
||||
if (
|
||||
process.env.NODE_ENV === "development" &&
|
||||
@ -34,6 +46,7 @@ prepareApp().then(() =>
|
||||
<SocketProvider>
|
||||
<Provider store={store}>
|
||||
<RemixBrowser />
|
||||
<PosthogInit />
|
||||
</Provider>
|
||||
</SocketProvider>
|
||||
</StrictMode>,
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
Outlet,
|
||||
ClientLoaderFunctionArgs,
|
||||
} from "@remix-run/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
@ -29,6 +30,7 @@ import DocsIcon from "#/assets/docs.svg?react";
|
||||
import { userIsAuthenticated } from "#/utils/user-is-authenticated";
|
||||
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
|
||||
import { WaitlistModal } from "#/components/waitlist-modal";
|
||||
import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal";
|
||||
import { setCurrentAgentState } from "#/state/agentSlice";
|
||||
import AgentState from "#/types/AgentState";
|
||||
|
||||
@ -44,6 +46,14 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
|
||||
|
||||
let token = localStorage.getItem("token");
|
||||
const ghToken = localStorage.getItem("ghToken");
|
||||
const analyticsConsent = localStorage.getItem("analytics-consent");
|
||||
const userConsents = analyticsConsent === "true";
|
||||
|
||||
if (!userConsents) {
|
||||
posthog.opt_out_capturing();
|
||||
} else {
|
||||
posthog.opt_in_capturing();
|
||||
}
|
||||
|
||||
let isAuthed: boolean = false;
|
||||
let githubAuthUrl: string | null = null;
|
||||
@ -82,6 +92,7 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
|
||||
user,
|
||||
settingsIsUpdated,
|
||||
settings,
|
||||
analyticsConsent,
|
||||
});
|
||||
};
|
||||
|
||||
@ -135,6 +146,7 @@ export default function MainApp() {
|
||||
githubAuthUrl,
|
||||
settingsIsUpdated,
|
||||
settings,
|
||||
analyticsConsent,
|
||||
} = useLoaderData<typeof clientLoader>();
|
||||
const logoutFetcher = useFetcher({ key: "logout" });
|
||||
const endSessionFetcher = useFetcher({ key: "end-session" });
|
||||
@ -309,6 +321,7 @@ export default function MainApp() {
|
||||
onClose={handleAccountSettingsModalClose}
|
||||
selectedLanguage={settings.LANGUAGE}
|
||||
gitHubError={isGitHubErrorReponse(user)}
|
||||
analyticsConsent={analyticsConsent}
|
||||
/>
|
||||
</ModalBackdrop>
|
||||
)}
|
||||
@ -333,6 +346,7 @@ export default function MainApp() {
|
||||
{!isAuthed && (
|
||||
<WaitlistModal ghToken={ghToken} githubAuthUrl={githubAuthUrl} />
|
||||
)}
|
||||
{!analyticsConsent && <AnalyticsConsentFormModal />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
9
frontend/src/routes/set-consent.ts
Normal file
9
frontend/src/routes/set-consent.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ClientActionFunctionArgs, json } from "@remix-run/react";
|
||||
|
||||
export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
|
||||
const formData = await request.formData();
|
||||
const userConsents = formData.get("analytics") === "on";
|
||||
localStorage.setItem("analytics-consent", userConsents.toString());
|
||||
|
||||
return json(null);
|
||||
};
|
||||
@ -28,6 +28,9 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => {
|
||||
const LANGUAGE = formData.get("language")?.toString();
|
||||
if (LANGUAGE) saveSettings({ LANGUAGE });
|
||||
|
||||
const ANALYTICS = formData.get("analytics")?.toString() ?? "false";
|
||||
localStorage.setItem("analytics-consent", ANALYTICS);
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user