From cdd8aace86d0d543e41c4621b8cf5d9d6740dbe2 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:48:56 +0400 Subject: [PATCH] refactor(frontend): migrate from direct posthog imports to usePostHog hook (#11703) --- frontend/__tests__/routes/accept-tos.test.tsx | 18 ++++-- .../__tests__/routes/app-settings.test.tsx | 10 +++- .../__tests__/utils/error-handler.test.ts | 8 +++ .../utils/handle-capture-consent.test.ts | 8 +-- frontend/package-lock.json | 56 ++++++++++--------- frontend/package.json | 3 +- .../analytics-consent-form-modal.tsx | 4 +- .../features/chat/chat-interface.tsx | 3 +- .../conversation-card/conversation-card.tsx | 3 +- .../shared/modals/settings/settings-form.tsx | 3 +- frontend/src/context/ws-client-provider.tsx | 14 ++++- frontend/src/entry.client.tsx | 34 ++++++----- frontend/src/hooks/mutation/use-logout.ts | 3 +- .../src/hooks/mutation/use-save-settings.ts | 3 +- frontend/src/hooks/query/use-git-user.ts | 3 +- .../use-conversation-name-context-menu.ts | 3 +- .../src/hooks/use-migrate-user-consent.ts | 8 ++- frontend/src/hooks/use-tracking.ts | 3 +- frontend/src/root.tsx | 1 + frontend/src/routes/accept-tos.tsx | 4 +- frontend/src/routes/app-settings.tsx | 4 +- frontend/src/services/actions.ts | 1 + frontend/src/utils/error-handler.ts | 18 ++++-- frontend/src/utils/handle-capture-consent.ts | 10 +++- 24 files changed, 150 insertions(+), 75 deletions(-) diff --git a/frontend/__tests__/routes/accept-tos.test.tsx b/frontend/__tests__/routes/accept-tos.test.tsx index ce6f36793b..7b15081485 100644 --- a/frontend/__tests__/routes/accept-tos.test.tsx +++ b/frontend/__tests__/routes/accept-tos.test.tsx @@ -1,10 +1,9 @@ import { render, screen } from "@testing-library/react"; import { it, describe, expect, vi, beforeEach, afterEach } from "vitest"; import userEvent from "@testing-library/user-event"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import AcceptTOS from "#/routes/accept-tos"; import * as CaptureConsent from "#/utils/handle-capture-consent"; -import * as ToastHandlers from "#/utils/custom-toast-handlers"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { openHands } from "#/api/open-hands-axios"; // Mock the react-router hooks @@ -44,9 +43,13 @@ const createWrapper = () => { }, }); - return ({ children }: { children: React.ReactNode }) => ( - {children} - ); + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + {children} + ); + } + + return Wrapper; }; describe("AcceptTOS", () => { @@ -106,7 +109,10 @@ describe("AcceptTOS", () => { // Wait for the mutation to complete await new Promise(process.nextTick); - expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true); + expect(handleCaptureConsentSpy).toHaveBeenCalledWith( + expect.anything(), + true, + ); expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", { redirect_url: "/dashboard", }); diff --git a/frontend/__tests__/routes/app-settings.test.tsx b/frontend/__tests__/routes/app-settings.test.tsx index e7b189a33b..44dacce2fb 100644 --- a/frontend/__tests__/routes/app-settings.test.tsx +++ b/frontend/__tests__/routes/app-settings.test.tsx @@ -178,7 +178,10 @@ describe("Form submission", () => { await userEvent.click(submit); await waitFor(() => - expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(true), + expect(handleCaptureConsentsSpy).toHaveBeenCalledWith( + expect.anything(), + true, + ), ); }); @@ -203,7 +206,10 @@ describe("Form submission", () => { await userEvent.click(submit); await waitFor(() => - expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(false), + expect(handleCaptureConsentsSpy).toHaveBeenCalledWith( + expect.anything(), + false, + ), ); }); diff --git a/frontend/__tests__/utils/error-handler.test.ts b/frontend/__tests__/utils/error-handler.test.ts index 0f1e91cae2..b0cf26bc64 100644 --- a/frontend/__tests__/utils/error-handler.test.ts +++ b/frontend/__tests__/utils/error-handler.test.ts @@ -32,6 +32,7 @@ describe("Error Handler", () => { const error = { message: "Test error", source: "test", + posthog, }; trackError(error); @@ -52,6 +53,7 @@ describe("Error Handler", () => { extra: "info", details: { foo: "bar" }, }, + posthog, }; trackError(error); @@ -73,6 +75,7 @@ describe("Error Handler", () => { const error = { message: "Toast error", source: "toast-test", + posthog, }; showErrorToast(error); @@ -94,6 +97,7 @@ describe("Error Handler", () => { message: "Toast error", source: "toast-test", metadata: { context: "testing" }, + posthog, }; showErrorToast(error); @@ -113,6 +117,7 @@ describe("Error Handler", () => { message: "Agent error", source: "agent-status", metadata: { id: "error.agent" }, + posthog, }); expect(posthog.captureException).toHaveBeenCalledWith( @@ -127,6 +132,7 @@ describe("Error Handler", () => { message: "Server error", source: "server", metadata: { error_code: 500, details: "Internal error" }, + posthog, }); expect(posthog.captureException).toHaveBeenCalledWith( @@ -145,6 +151,7 @@ describe("Error Handler", () => { message: error.message, source: "feedback", metadata: { conversationId: "123", error }, + posthog, }); expect(posthog.captureException).toHaveBeenCalledWith( @@ -164,6 +171,7 @@ describe("Error Handler", () => { message: "Chat error", source: "chat-test", msgId: "123", + posthog, }; showChatError(error); diff --git a/frontend/__tests__/utils/handle-capture-consent.test.ts b/frontend/__tests__/utils/handle-capture-consent.test.ts index 3b337424a7..0faf999c2b 100644 --- a/frontend/__tests__/utils/handle-capture-consent.test.ts +++ b/frontend/__tests__/utils/handle-capture-consent.test.ts @@ -13,14 +13,14 @@ describe("handleCaptureConsent", () => { }); it("should opt out of of capturing", () => { - handleCaptureConsent(false); + handleCaptureConsent(posthog, false); expect(optOutSpy).toHaveBeenCalled(); expect(optInSpy).not.toHaveBeenCalled(); }); it("should opt in to capturing if the user consents", () => { - handleCaptureConsent(true); + handleCaptureConsent(posthog, true); expect(optInSpy).toHaveBeenCalled(); expect(optOutSpy).not.toHaveBeenCalled(); @@ -28,7 +28,7 @@ describe("handleCaptureConsent", () => { it("should not opt in to capturing if the user is already opted in", () => { hasOptedInSpy.mockReturnValueOnce(true); - handleCaptureConsent(true); + handleCaptureConsent(posthog, true); expect(optInSpy).not.toHaveBeenCalled(); expect(optOutSpy).not.toHaveBeenCalled(); @@ -36,7 +36,7 @@ describe("handleCaptureConsent", () => { it("should not opt out of capturing if the user is already opted out", () => { hasOptedOutSpy.mockReturnValueOnce(true); - handleCaptureConsent(false); + handleCaptureConsent(posthog, false); expect(optOutSpy).not.toHaveBeenCalled(); expect(optInSpy).not.toHaveBeenCalled(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index feb13dcc3a..66b115ae73 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@heroui/use-infinite-scroll": "^2.2.11", "@microlink/react-json-view": "^1.26.2", "@monaco-editor/react": "^4.7.0-rc.0", + "@posthog/react": "^1.4.0", "@react-router/node": "^7.9.3", "@react-router/serve": "^7.9.3", "@react-types/shared": "^3.32.0", @@ -38,7 +39,7 @@ "jose": "^6.1.0", "lucide-react": "^0.544.0", "monaco-editor": "^0.53.0", - "posthog-js": "^1.268.8", + "posthog-js": "^1.290.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-highlight": "^0.15.0", @@ -3511,9 +3512,29 @@ "license": "MIT" }, "node_modules/@posthog/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.2.tgz", - "integrity": "sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.5.2.tgz", + "integrity": "sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, + "node_modules/@posthog/react": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@posthog/react/-/react-1.4.0.tgz", + "integrity": "sha512-xzPeZ753fQ0deZzdgY/0YavZvNpmdaxUzLYJYu5XjONNcZ8PwJnNLEK+7D/Cj8UM4Q8nWI7QC5mjum0uLWa4FA==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=16.8.0", + "posthog-js": ">=1.257.2", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.28", @@ -8183,7 +8204,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8198,7 +8218,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -11403,7 +11422,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -14073,7 +14091,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14264,27 +14281,16 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.268.8", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.8.tgz", - "integrity": "sha512-BJiKK4MlUvs7ybnQcy1KkwAz+SZkE/wRLotetIoank5kbqZs8FLbeyozFvmmgx4aoMmaVymYBSmYphYjYQeidw==", + "version": "1.290.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.290.0.tgz", + "integrity": "sha512-zavBwZkf+3JeiSDVE7ZDXBfzva/iOljicdhdJH+cZoqp0LsxjKxjnNhGOd3KpAhw0wqdwjhd7Lp1aJuI7DXyaw==", + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@posthog/core": "1.2.2", + "@posthog/core": "1.5.2", "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/web-vitals": { @@ -15547,7 +15553,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -15560,7 +15565,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" diff --git a/frontend/package.json b/frontend/package.json index 5ad91c3636..ec7e1793d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "@heroui/use-infinite-scroll": "^2.2.11", "@microlink/react-json-view": "^1.26.2", "@monaco-editor/react": "^4.7.0-rc.0", + "@posthog/react": "^1.4.0", "@react-router/node": "^7.9.3", "@react-router/serve": "^7.9.3", "@react-types/shared": "^3.32.0", @@ -37,7 +38,7 @@ "jose": "^6.1.0", "lucide-react": "^0.544.0", "monaco-editor": "^0.53.0", - "posthog-js": "^1.268.8", + "posthog-js": "^1.290.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-highlight": "^0.15.0", diff --git a/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx b/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx index c3ab215272..cc2f293235 100644 --- a/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx +++ b/frontend/src/components/features/analytics/analytics-consent-form-modal.tsx @@ -1,4 +1,5 @@ import { useTranslation } from "react-i18next"; +import { usePostHog } from "posthog-js/react"; import { BaseModalTitle, BaseModalDescription, @@ -17,6 +18,7 @@ interface AnalyticsConsentFormModalProps { export function AnalyticsConsentFormModal({ onClose, }: AnalyticsConsentFormModalProps) { + const posthog = usePostHog(); const { t } = useTranslation(); const { mutate: saveUserSettings } = useSaveSettings(); @@ -29,7 +31,7 @@ export function AnalyticsConsentFormModal({ { user_consents_to_analytics: analytics }, { onSuccess: () => { - handleCaptureConsent(analytics); + handleCaptureConsent(posthog, analytics); onClose(); }, }, diff --git a/frontend/src/components/features/chat/chat-interface.tsx b/frontend/src/components/features/chat/chat-interface.tsx index cabf087689..f37bd59c26 100644 --- a/frontend/src/components/features/chat/chat-interface.tsx +++ b/frontend/src/components/features/chat/chat-interface.tsx @@ -1,5 +1,5 @@ import React from "react"; -import posthog from "posthog-js"; +import { usePostHog } from "posthog-js/react"; import { useParams } from "react-router"; import { useTranslation } from "react-i18next"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; @@ -60,6 +60,7 @@ function getEntryPoint( } export function ChatInterface() { + const posthog = usePostHog(); const { setMessageToSend } = useConversationStore(); const { data: conversation } = useActiveConversation(); const { errorMessage } = useErrorMessageStore(); diff --git a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card.tsx b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card.tsx index 8c6b895eaf..fff0a0888d 100644 --- a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card.tsx @@ -1,5 +1,5 @@ import React from "react"; -import posthog from "posthog-js"; +import { usePostHog } from "posthog-js/react"; import { cn } from "#/utils/utils"; import { transformVSCodeUrl } from "#/utils/vscode-url-helper"; import ConversationService from "#/api/conversation-service/conversation-service.api"; @@ -44,6 +44,7 @@ export function ConversationCard({ contextMenuOpen = false, onContextMenuToggle, }: ConversationCardProps) { + const posthog = usePostHog(); const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view"); const onTitleSave = (newTitle: string) => { diff --git a/frontend/src/components/shared/modals/settings/settings-form.tsx b/frontend/src/components/shared/modals/settings/settings-form.tsx index 838b4f0b06..e08b59c8e0 100644 --- a/frontend/src/components/shared/modals/settings/settings-form.tsx +++ b/frontend/src/components/shared/modals/settings/settings-form.tsx @@ -1,7 +1,7 @@ import { useLocation } from "react-router"; import { useTranslation } from "react-i18next"; import React from "react"; -import posthog from "posthog-js"; +import { usePostHog } from "posthog-js/react"; import { I18nKey } from "#/i18n/declaration"; import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers"; import { DangerModal } from "../confirmation-modals/danger-modal"; @@ -22,6 +22,7 @@ interface SettingsFormProps { } export function SettingsForm({ settings, models, onClose }: SettingsFormProps) { + const posthog = usePostHog(); const { mutate: saveUserSettings } = useSaveSettings(); const location = useLocation(); diff --git a/frontend/src/context/ws-client-provider.tsx b/frontend/src/context/ws-client-provider.tsx index 8f0a2829c0..38f390476f 100644 --- a/frontend/src/context/ws-client-provider.tsx +++ b/frontend/src/context/ws-client-provider.tsx @@ -1,6 +1,7 @@ import React from "react"; import { io, Socket } from "socket.io-client"; import { useQueryClient } from "@tanstack/react-query"; +import { usePostHog } from "posthog-js/react"; import EventLogger from "#/utils/event-logger"; import { handleAssistantMessage } from "#/services/actions"; import { showChatError, trackError } from "#/utils/error-handler"; @@ -100,7 +101,10 @@ interface ErrorArgData { msg_id: string; } -export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) { +export function updateStatusWhenErrorMessagePresent( + data: ErrorArg | unknown, + posthog?: ReturnType, +) { const isObject = (val: unknown): val is object => !!val && typeof val === "object"; const isString = (val: unknown): val is string => typeof val === "string"; @@ -123,6 +127,7 @@ export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) { source: "websocket", metadata, msgId, + posthog, }); } } @@ -131,6 +136,7 @@ export function WsClientProvider({ conversationId, children, }: React.PropsWithChildren) { + const posthog = usePostHog(); const { setErrorMessage, removeErrorMessage } = useErrorMessageStore(); const { removeOptimisticUserMessage } = useOptimisticUserMessageStore(); const { addEvent, clearEvents } = useEventStore(); @@ -178,6 +184,7 @@ export function WsClientProvider({ message: errorMessage, source: "chat", metadata: { msgId: event.id }, + posthog, }); setErrorMessage(errorMessage); @@ -193,6 +200,7 @@ export function WsClientProvider({ message: event.message, source: "chat", metadata: { msgId: event.id }, + posthog, }); } else { removeErrorMessage(); @@ -260,14 +268,14 @@ export function WsClientProvider({ sio.io.opts.query = sio.io.opts.query || {}; sio.io.opts.query.latest_event_id = lastEventRef.current?.id; - updateStatusWhenErrorMessagePresent(data); + updateStatusWhenErrorMessagePresent(data, posthog); setErrorMessage(hasValidMessageProperty(data) ? data.message : ""); } function handleError(data: unknown) { // set status setWebSocketStatus("DISCONNECTED"); - updateStatusWhenErrorMessagePresent(data); + updateStatusWhenErrorMessagePresent(data, posthog); setErrorMessage( hasValidMessageProperty(data) diff --git a/frontend/src/entry.client.tsx b/frontend/src/entry.client.tsx index dc1e2e4dd5..9fe6212d4e 100644 --- a/frontend/src/entry.client.tsx +++ b/frontend/src/entry.client.tsx @@ -8,17 +8,18 @@ import { HydratedRouter } from "react-router/dom"; import React, { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; -import posthog from "posthog-js"; +import { PostHogProvider } from "posthog-js/react"; import "./i18n"; import { QueryClientProvider } from "@tanstack/react-query"; import OptionService from "./api/option-service/option-service.api"; import { displayErrorToast } from "./utils/custom-toast-handlers"; import { queryClient } from "./query-client-config"; -function PosthogInit() { +function PostHogWrapper({ children }: { children: React.ReactNode }) { const [posthogClientKey, setPosthogClientKey] = React.useState( null, ); + const [isLoading, setIsLoading] = React.useState(true); React.useEffect(() => { (async () => { @@ -27,20 +28,27 @@ function PosthogInit() { setPosthogClientKey(config.POSTHOG_CLIENT_KEY); } catch { displayErrorToast("Error fetching PostHog client key"); + } finally { + setIsLoading(false); } })(); }, []); - React.useEffect(() => { - if (posthogClientKey) { - posthog.init(posthogClientKey, { + if (isLoading || !posthogClientKey) { + return children; + } + + return ( + + {children} + + ); } async function prepareApp() { @@ -62,10 +70,10 @@ prepareApp().then(() => document, - - + + + -