diff --git a/frontend/__tests__/components/features/auth-modal.test.tsx b/frontend/__tests__/components/features/auth-modal.test.tsx index dc7be3b474..32b682d506 100644 --- a/frontend/__tests__/components/features/auth-modal.test.tsx +++ b/frontend/__tests__/components/features/auth-modal.test.tsx @@ -8,6 +8,13 @@ vi.mock("#/hooks/use-auth-url", () => ({ useAuthUrl: () => "https://gitlab.com/oauth/authorize", })); +// Mock the useTracking hook +vi.mock("#/hooks/use-tracking", () => ({ + useTracking: () => ({ + trackLoginButtonClick: vi.fn(), + }), +})); + describe("AuthModal", () => { beforeEach(() => { vi.stubGlobal("location", { href: "" }); diff --git a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx index d22825d8d1..afdb8e84ba 100644 --- a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx +++ b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx @@ -21,6 +21,7 @@ const mockUseConfig = vi.fn(); const mockUseRepositoryMicroagents = vi.fn(); const mockUseMicroagentManagementConversations = vi.fn(); const mockUseSearchRepositories = vi.fn(); +const mockUseCreateConversationAndSubscribeMultiple = vi.fn(); vi.mock("#/hooks/use-user-providers", () => ({ useUserProviders: () => mockUseUserProviders(), @@ -47,6 +48,17 @@ vi.mock("#/hooks/query/use-search-repositories", () => ({ useSearchRepositories: () => mockUseSearchRepositories(), })); +vi.mock("#/hooks/use-tracking", () => ({ + useTracking: () => ({ + trackEvent: vi.fn(), + }), +})); + +vi.mock("#/hooks/use-create-conversation-and-subscribe-multiple", () => ({ + useCreateConversationAndSubscribeMultiple: () => + mockUseCreateConversationAndSubscribeMultiple(), +})); + describe("MicroagentManagement", () => { const RouterStub = createRoutesStub([ { @@ -309,6 +321,16 @@ describe("MicroagentManagement", () => { isError: false, }); + mockUseCreateConversationAndSubscribeMultiple.mockReturnValue({ + createConversationAndSubscribe: vi.fn(({ onSuccessCallback }) => { + // Immediately call the success callback to close the modal + if (onSuccessCallback) { + onSuccessCallback(); + } + }), + isPending: false, + }); + // Mock the search repositories hook to return repositories with OpenHands suffixes const mockSearchResults = getRepositoriesWithOpenHandsSuffix(mockRepositories); diff --git a/frontend/src/components/features/chat/git-control-bar-pr-button.tsx b/frontend/src/components/features/chat/git-control-bar-pr-button.tsx index ea7b84c7e5..3beb7628dd 100644 --- a/frontend/src/components/features/chat/git-control-bar-pr-button.tsx +++ b/frontend/src/components/features/chat/git-control-bar-pr-button.tsx @@ -1,10 +1,10 @@ import { useTranslation } from "react-i18next"; -import posthog from "posthog-js"; import PRIcon from "#/icons/u-pr.svg?react"; import { cn, getCreatePRPrompt } from "#/utils/utils"; import { useUserProviders } from "#/hooks/use-user-providers"; import { I18nKey } from "#/i18n/declaration"; import { Provider } from "#/types/settings"; +import { useTracking } from "#/hooks/use-tracking"; interface GitControlBarPrButtonProps { onSuggestionsClick: (value: string) => void; @@ -20,6 +20,7 @@ export function GitControlBarPrButton({ isConversationReady = true, }: GitControlBarPrButtonProps) { const { t } = useTranslation(); + const { trackCreatePrButtonClick } = useTracking(); const { providers } = useUserProviders(); @@ -28,7 +29,7 @@ export function GitControlBarPrButton({ providersAreSet && hasRepository && isConversationReady; const handlePrClick = () => { - posthog.capture("create_pr_button_clicked"); + trackCreatePrButtonClick(); onSuggestionsClick(getCreatePRPrompt(currentGitProvider)); }; diff --git a/frontend/src/components/features/chat/git-control-bar-pull-button.tsx b/frontend/src/components/features/chat/git-control-bar-pull-button.tsx index 7adb9a4649..d0a1374098 100644 --- a/frontend/src/components/features/chat/git-control-bar-pull-button.tsx +++ b/frontend/src/components/features/chat/git-control-bar-pull-button.tsx @@ -1,10 +1,10 @@ import { useTranslation } from "react-i18next"; -import posthog from "posthog-js"; import ArrowDownIcon from "#/icons/u-arrow-down.svg?react"; import { cn, getGitPullPrompt } from "#/utils/utils"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { useUserProviders } from "#/hooks/use-user-providers"; import { I18nKey } from "#/i18n/declaration"; +import { useTracking } from "#/hooks/use-tracking"; interface GitControlBarPullButtonProps { onSuggestionsClick: (value: string) => void; @@ -16,6 +16,7 @@ export function GitControlBarPullButton({ isConversationReady = true, }: GitControlBarPullButtonProps) { const { t } = useTranslation(); + const { trackPullButtonClick } = useTracking(); const { data: conversation } = useActiveConversation(); const { providers } = useUserProviders(); @@ -26,7 +27,7 @@ export function GitControlBarPullButton({ providersAreSet && hasRepository && isConversationReady; const handlePullClick = () => { - posthog.capture("pull_button_clicked"); + trackPullButtonClick(); onSuggestionsClick(getGitPullPrompt()); }; diff --git a/frontend/src/components/features/chat/git-control-bar-push-button.tsx b/frontend/src/components/features/chat/git-control-bar-push-button.tsx index 5c40bd845f..dec4e97bed 100644 --- a/frontend/src/components/features/chat/git-control-bar-push-button.tsx +++ b/frontend/src/components/features/chat/git-control-bar-push-button.tsx @@ -1,10 +1,10 @@ import { useTranslation } from "react-i18next"; -import posthog from "posthog-js"; import ArrowUpIcon from "#/icons/u-arrow-up.svg?react"; import { cn, getGitPushPrompt } from "#/utils/utils"; import { useUserProviders } from "#/hooks/use-user-providers"; import { I18nKey } from "#/i18n/declaration"; import { Provider } from "#/types/settings"; +import { useTracking } from "#/hooks/use-tracking"; interface GitControlBarPushButtonProps { onSuggestionsClick: (value: string) => void; @@ -20,6 +20,7 @@ export function GitControlBarPushButton({ isConversationReady = true, }: GitControlBarPushButtonProps) { const { t } = useTranslation(); + const { trackPushButtonClick } = useTracking(); const { providers } = useUserProviders(); @@ -28,7 +29,7 @@ export function GitControlBarPushButton({ providersAreSet && hasRepository && isConversationReady; const handlePushClick = () => { - posthog.capture("push_button_clicked"); + trackPushButtonClick(); onSuggestionsClick(getGitPushPrompt(currentGitProvider)); }; diff --git a/frontend/src/components/features/waitlist/auth-modal.tsx b/frontend/src/components/features/waitlist/auth-modal.tsx index cbc9e0db32..d20ef04a28 100644 --- a/frontend/src/components/features/waitlist/auth-modal.tsx +++ b/frontend/src/components/features/waitlist/auth-modal.tsx @@ -11,6 +11,7 @@ import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react"; import { useAuthUrl } from "#/hooks/use-auth-url"; import { GetConfigResponse } from "#/api/option-service/option.types"; import { Provider } from "#/types/settings"; +import { useTracking } from "#/hooks/use-tracking"; interface AuthModalProps { githubAuthUrl: string | null; @@ -26,6 +27,7 @@ export function AuthModal({ providersConfigured, }: AuthModalProps) { const { t } = useTranslation(); + const { trackLoginButtonClick } = useTracking(); const gitlabAuthUrl = useAuthUrl({ appMode: appMode || null, @@ -47,6 +49,7 @@ export function AuthModal({ const handleGitHubAuth = () => { if (githubAuthUrl) { + trackLoginButtonClick({ provider: "github" }); // Always start the OIDC flow, let the backend handle TOS check window.location.href = githubAuthUrl; } @@ -54,6 +57,7 @@ export function AuthModal({ const handleGitLabAuth = () => { if (gitlabAuthUrl) { + trackLoginButtonClick({ provider: "gitlab" }); // Always start the OIDC flow, let the backend handle TOS check window.location.href = gitlabAuthUrl; } @@ -61,6 +65,7 @@ export function AuthModal({ const handleBitbucketAuth = () => { if (bitbucketAuthUrl) { + trackLoginButtonClick({ provider: "bitbucket" }); // Always start the OIDC flow, let the backend handle TOS check window.location.href = bitbucketAuthUrl; } @@ -68,6 +73,7 @@ export function AuthModal({ const handleEnterpriseSsoAuth = () => { if (enterpriseSsoUrl) { + trackLoginButtonClick({ provider: "enterprise_sso" }); // Always start the OIDC flow, let the backend handle TOS check window.location.href = enterpriseSsoUrl; } diff --git a/frontend/src/hooks/mutation/use-add-git-providers.ts b/frontend/src/hooks/mutation/use-add-git-providers.ts index 323a33b97f..b7788b88c4 100644 --- a/frontend/src/hooks/mutation/use-add-git-providers.ts +++ b/frontend/src/hooks/mutation/use-add-git-providers.ts @@ -1,9 +1,11 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { SecretsService } from "#/api/secrets-service"; import { Provider, ProviderToken } from "#/types/settings"; +import { useTracking } from "#/hooks/use-tracking"; export const useAddGitProviders = () => { const queryClient = useQueryClient(); + const { trackGitProviderConnected } = useTracking(); return useMutation({ mutationFn: ({ @@ -11,7 +13,18 @@ export const useAddGitProviders = () => { }: { providers: Record; }) => SecretsService.addGitProvider(providers), - onSuccess: async () => { + onSuccess: async (_, { providers }) => { + // Track which providers were connected (filter out empty tokens) + const connectedProviders = Object.entries(providers) + .filter(([, value]) => value.token && value.token.trim() !== "") + .map(([key]) => key); + + if (connectedProviders.length > 0) { + trackGitProviderConnected({ + providers: connectedProviders, + }); + } + await queryClient.invalidateQueries({ queryKey: ["settings"] }); }, meta: { diff --git a/frontend/src/hooks/mutation/use-create-conversation.ts b/frontend/src/hooks/mutation/use-create-conversation.ts index 24d59e75eb..4baba32802 100644 --- a/frontend/src/hooks/mutation/use-create-conversation.ts +++ b/frontend/src/hooks/mutation/use-create-conversation.ts @@ -1,11 +1,11 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import posthog from "posthog-js"; import ConversationService from "#/api/conversation-service/conversation-service.api"; import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api"; import { SuggestedTask } from "#/utils/types"; import { Provider } from "#/types/settings"; import { CreateMicroagent, Conversation } from "#/api/open-hands.types"; import { USE_V1_CONVERSATION_API } from "#/utils/feature-flags"; +import { useTracking } from "#/hooks/use-tracking"; interface CreateConversationVariables { query?: string; @@ -31,6 +31,7 @@ interface CreateConversationResponse extends Partial { export const useCreateConversation = () => { const queryClient = useQueryClient(); + const { trackConversationCreated } = useTracking(); return useMutation({ mutationKey: ["create-conversation"], @@ -86,12 +87,11 @@ export const useCreateConversation = () => { is_v1: false, }; }, - onSuccess: async (_, { query, repository }) => { - posthog.capture("initial_query_submitted", { - entry_point: "task_form", - query_character_length: query?.length, - has_repository: !!repository, + onSuccess: async (_, { repository }) => { + trackConversationCreated({ + hasRepository: !!repository, }); + queryClient.removeQueries({ queryKey: ["user", "conversations"], }); diff --git a/frontend/src/hooks/use-tracking.ts b/frontend/src/hooks/use-tracking.ts new file mode 100644 index 0000000000..714d3a06e7 --- /dev/null +++ b/frontend/src/hooks/use-tracking.ts @@ -0,0 +1,77 @@ +import posthog from "posthog-js"; +import { useConfig } from "./query/use-config"; +import { useSettings } from "./query/use-settings"; +import { Provider } from "#/types/settings"; + +/** + * Hook that provides tracking functions with automatic data collection + * from available hooks (config, settings, etc.) + */ +export const useTracking = () => { + const { data: config } = useConfig(); + const { data: settings } = useSettings(); + + // Common properties included in all tracking events + const commonProperties = { + app_surface: config?.APP_MODE || "unknown", + plan_tier: null, + current_url: window.location.href, + user_email: settings?.EMAIL || settings?.GIT_USER_EMAIL || null, + }; + + const trackLoginButtonClick = ({ provider }: { provider: Provider }) => { + posthog.capture("login_button_clicked", { + provider, + ...commonProperties, + }); + }; + + const trackConversationCreated = ({ + hasRepository, + }: { + hasRepository: boolean; + }) => { + posthog.capture("conversation_created", { + has_repository: hasRepository, + ...commonProperties, + }); + }; + + const trackPushButtonClick = () => { + posthog.capture("push_button_clicked", { + ...commonProperties, + }); + }; + + const trackPullButtonClick = () => { + posthog.capture("pull_button_clicked", { + ...commonProperties, + }); + }; + + const trackCreatePrButtonClick = () => { + posthog.capture("create_pr_button_clicked", { + ...commonProperties, + }); + }; + + const trackGitProviderConnected = ({ + providers, + }: { + providers: string[]; + }) => { + posthog.capture("git_provider_connected", { + providers, + ...commonProperties, + }); + }; + + return { + trackLoginButtonClick, + trackConversationCreated, + trackPushButtonClick, + trackPullButtonClick, + trackCreatePrButtonClick, + trackGitProviderConnected, + }; +};