diff --git a/apps/docs/anonymous-analytics.mdx b/apps/docs/anonymous-analytics.mdx index b2053d7c2..e1bfbf7d7 100644 --- a/apps/docs/anonymous-analytics.mdx +++ b/apps/docs/anonymous-analytics.mdx @@ -15,8 +15,13 @@ When anonymous telemetry is enabled, your deployment sends: - **Usage events** — page views (as route patterns like `/task/[taskId]`, never actual URLs or IDs) and product events such as tasks being created - or settling as completed, failed, or canceled, with non-identifying facts - like the harness, model, source surface, and sandbox provider used. + or settling as completed, failed, or canceled, plus setup progress such as + reaching authenticated setup and configuring or connecting communications, + source-control, inference, and sandbox providers. Setup events include only + the provider type and, where Roomote can determine it from deployment state, + whether it was configured before the wizard. Other events include + non-identifying facts like the harness, model, source surface, and sandbox + provider used. - **A daily instance report** — aggregate deployment metadata and usage: setup timestamps; counts of users, environments, and connected repositories; task, model, token, and cost totals for the past day; pull-request statistics diff --git a/apps/web/src/app/(onboarding)/setup/SetupBootstrapFlow.tsx b/apps/web/src/app/(onboarding)/setup/SetupBootstrapFlow.tsx index 464d1a279..f023ac2e5 100644 --- a/apps/web/src/app/(onboarding)/setup/SetupBootstrapFlow.tsx +++ b/apps/web/src/app/(onboarding)/setup/SetupBootstrapFlow.tsx @@ -2,7 +2,7 @@ import { AnimatePresence, motion, type Variants } from 'motion/react'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useRedirectToSignIn } from '@/hooks/useSignInRedirect'; @@ -94,6 +94,9 @@ export function SetupBootstrapFlow() { }, ), ); + const trackWelcomeSeen = useMutation( + trpc.setupBootstrap.trackWelcomeSeen.mutationOptions(), + ); const bootstrapAuthProvider = getBootstrapAuthProvider( bootstrapStatus?.authSetup, pendingSetupAuthProvider, @@ -225,11 +228,14 @@ export function SetupBootstrapFlow() { > {bootstrapStep === 'welcome' && ( + onContinue={() => { + trackWelcomeSeen.mutate( + setupToken ? { setupToken } : undefined, + ); setBootstrapStepWithTransition( getBootstrapStepAfterWelcome(bootstrapStatus.authSetup), - ) - } + ); + }} /> )} {bootstrapStep === 'email-account' && ( diff --git a/apps/web/src/app/(onboarding)/setup/SetupSignedInFlow.tsx b/apps/web/src/app/(onboarding)/setup/SetupSignedInFlow.tsx index cb536f36c..1494b14ab 100644 --- a/apps/web/src/app/(onboarding)/setup/SetupSignedInFlow.tsx +++ b/apps/web/src/app/(onboarding)/setup/SetupSignedInFlow.tsx @@ -80,6 +80,9 @@ export function SetupSignedInFlow() { useState(null); const [pendingModelProvider, setPendingModelProvider] = useState(null); + const trackWelcomeSeen = useMutation( + trpc.setupNew.trackWelcomeSeen.mutationOptions(), + ); const { data: setupStatus, isLoading: isSetupStatusLoading, @@ -291,7 +294,14 @@ export function SetupSignedInFlow() { animate="center" exit="exit" > - {step === 'welcome' && } + {step === 'welcome' && ( + { + trackWelcomeSeen.mutate(); + goToNextStep(); + }} + /> + )} {step === 'auth-provider' && ( ({ - connectSlackMutateMock: vi.fn(), - teamsStatusState: { - data: { - botConfigured: true, - botUsesTenantSpecificTokenFlow: false, - microsoftAuthConfigured: true, - webhookUrl: 'https://roomote.example.com/api/webhooks/teams', - openInTeamsUrl: - 'https://teams.microsoft.com/l/chat/0/0?users=28%3Abot-app-id' as - | string - | null, - botName: 'Roomote', - primaryConversationReady: true, - primaryConversationType: 'channel' as string | null, +const { connectSlackMutateMock, trackMilestoneMock, teamsStatusState } = + vi.hoisted(() => ({ + connectSlackMutateMock: vi.fn(), + trackMilestoneMock: vi.fn(), + teamsStatusState: { + data: { + botConfigured: true, + botUsesTenantSpecificTokenFlow: false, + microsoftAuthConfigured: true, + webhookUrl: 'https://roomote.example.com/api/webhooks/teams', + openInTeamsUrl: + 'https://teams.microsoft.com/l/chat/0/0?users=28%3Abot-app-id' as + | string + | null, + botName: 'Roomote', + primaryConversationReady: true, + primaryConversationType: 'channel' as string | null, + }, + isPending: false, + isError: false, }, - isPending: false, - isError: false, - }, + })); + +vi.mock('@tanstack/react-query', () => ({ + useMutation: () => ({ mutate: trackMilestoneMock }), +})); + +vi.mock('@/trpc/client', () => ({ + useTRPC: () => ({ + setupNew: { + trackCommsState: { mutationOptions: () => ({}) }, + }, + }), })); vi.mock('@/hooks/slack', () => ({ diff --git a/apps/web/src/app/(onboarding)/setup/StepCommunicationConnect.tsx b/apps/web/src/app/(onboarding)/setup/StepCommunicationConnect.tsx index 8bc4f825a..34b232425 100644 --- a/apps/web/src/app/(onboarding)/setup/StepCommunicationConnect.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepCommunicationConnect.tsx @@ -1,10 +1,13 @@ 'use client'; +import { useEffect, useRef } from 'react'; +import { useMutation } from '@tanstack/react-query'; import type { SetupAuthStatus } from '@roomote/types'; import { toast } from 'sonner'; import { useConnectSlack } from '@/hooks/slack'; import { useTeamsIntegrationStatus } from '@/hooks/teams'; +import { useTRPC } from '@/trpc/client'; import { TaskStatusIndicator } from '@/components/sandbox'; import { ArrowRight, @@ -41,6 +44,7 @@ export function StepCommunicationConnect({ onBack?: () => void; returnPath?: string; }) { + const trpc = useTRPC(); const provider = getCommunicationProvider(authSetup); const connectSlack = useConnectSlack(returnPath, { onSuccess: (url) => { @@ -49,6 +53,41 @@ export function StepCommunicationConnect({ onError: () => toast.error('Failed to connect Slack. Please try again.'), }); const teamsIntegrationStatus = useTeamsIntegrationStatus(); + const configuredTrackedRef = useRef(false); + const authedTrackedRef = useRef(false); + const trackCommsState = useMutation( + trpc.setupNew.trackCommsState.mutationOptions(), + ); + const teamsStatus = teamsIntegrationStatus.data; + const teamsConfigured = + teamsStatus?.botConfigured === true && teamsStatus.microsoftAuthConfigured; + const primaryConversationReady = Boolean( + teamsStatus?.primaryConversationReady, + ); + useEffect(() => { + if ( + provider === 'microsoft' && + teamsConfigured && + !configuredTrackedRef.current + ) { + configuredTrackedRef.current = true; + trackCommsState.mutate({ + provider: 'microsoft', + }); + } + }, [provider, teamsConfigured, trackCommsState]); + useEffect(() => { + if ( + provider === 'microsoft' && + primaryConversationReady && + !authedTrackedRef.current + ) { + authedTrackedRef.current = true; + trackCommsState.mutate({ + provider: 'microsoft', + }); + } + }, [primaryConversationReady, provider, trackCommsState]); const skipLink = (