feat: Analytics with PostHog (#4655)

This commit is contained in:
sp.wack 2024-11-04 11:57:56 +02:00 committed by GitHub
parent 387c8f1df3
commit 0595d2336a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 143 additions and 3 deletions

View File

@ -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",

View File

@ -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",

View 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>
);
}

View File

@ -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={

View File

@ -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 {

View File

@ -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>,

View File

@ -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>
);
}

View 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);
};

View File

@ -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 });
}