From 848b151a62473bfe769185543f961f8a22f026ee Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 31 Mar 2026 15:30:19 +0200 Subject: [PATCH] feat(cloud-agent): add PostHog tracking for remote session events Add onResolved callback to CloudAgentSession that fires after transport resolution, giving the session manager access to the authoritative ResolvedSession. Use this to track an ActiveSessionType ('cloud-agent' | 'cli') and emit PostHog analytics events (remote_session_opened, remote_session_message_sent) for live CLI sessions. --- .../cloud-agent-next/CloudAgentProvider.tsx | 17 +++++++++++++++++ src/lib/cloud-agent-sdk/session-manager.ts | 19 +++++++++++++++++++ src/lib/cloud-agent-sdk/session.ts | 2 ++ 3 files changed, 38 insertions(+) diff --git a/src/components/cloud-agent-next/CloudAgentProvider.tsx b/src/components/cloud-agent-next/CloudAgentProvider.tsx index ffede7dc2..38ea81721 100644 --- a/src/components/cloud-agent-next/CloudAgentProvider.tsx +++ b/src/components/cloud-agent-next/CloudAgentProvider.tsx @@ -13,6 +13,7 @@ import { type CloudAgentSessionId, } from '@/lib/cloud-agent-sdk'; import { SESSION_INGEST_WS_URL } from '@/lib/constants'; +import { usePostHog } from 'posthog-js/react'; const ManagerContext = createContext(null); @@ -24,6 +25,9 @@ type CloudAgentProviderProps = { export function CloudAgentProvider({ children, organizationId }: CloudAgentProviderProps) { const storeRef = useRef(createStore()); const trpcClient = useRawTRPCClient(); + const posthog = usePostHog(); + const posthogRef = useRef(posthog); + posthogRef.current = posthog; // Create manager once per provider instance. // trpcClient is stable (from context); organizationId is stable per provider mount. @@ -259,6 +263,19 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi window.history.replaceState(window.history.state, '', url.toString()); } }, + + onRemoteSessionOpened: ({ kiloSessionId }) => { + posthogRef.current?.capture('remote_session_opened', { + feature: 'remote-session', + kilo_session_id: kiloSessionId, + }); + }, + onRemoteSessionMessageSent: ({ kiloSessionId }) => { + posthogRef.current?.capture('remote_session_message_sent', { + feature: 'remote-session', + kilo_session_id: kiloSessionId, + }); + }, }); } diff --git a/src/lib/cloud-agent-sdk/session-manager.ts b/src/lib/cloud-agent-sdk/session-manager.ts index d2c732d22..307da0b40 100644 --- a/src/lib/cloud-agent-sdk/session-manager.ts +++ b/src/lib/cloud-agent-sdk/session-manager.ts @@ -40,6 +40,7 @@ type SessionConfig = { model: string; variant?: string | null; }; +type ActiveSessionType = 'cloud-agent' | 'cli'; type StandaloneQuestion = { requestId: string; questions: QuestionInfo[] }; type StandalonePermission = { requestId: string; @@ -95,6 +96,8 @@ type SessionManagerConfig = { onComplete?: () => void; onBranchChanged?: (branch: string) => void; onSendFailed?: (messageText: string) => void; + onRemoteSessionOpened?: (data: { kiloSessionId: KiloSessionId }) => void; + onRemoteSessionMessageSent?: (data: { kiloSessionId: KiloSessionId }) => void; }; // Writable/read-only atom aliases for the public atoms record @@ -299,6 +302,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { // Private mutable state let activeSessionId: KiloSessionId | null = null; let currentSession: CloudAgentSession | null = null; + let activeSessionType: ActiveSessionType | null = null; let stateUnsub: (() => void) | null = null; let indicatorTimer: ReturnType | null = null; @@ -418,6 +422,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { async function switchSession(kiloSessionId: KiloSessionId): Promise { activeSessionId = kiloSessionId; + activeSessionType = null; stateUnsub?.(); stateUnsub = null; currentSession?.destroy(); @@ -500,6 +505,10 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { const ap = store.get(activePermissionAtom); if (ap?.requestId === requestId) store.set(activePermissionAtom, null); }, + onResolved: resolved => { + if (resolved.cloudAgentSessionId) activeSessionType = 'cloud-agent'; + else if (resolved.isLive) activeSessionType = 'cli'; + }, onBranchChanged: branch => { const currentFetched = store.get(fetchedSessionDataAtom); if (currentFetched) { @@ -538,6 +547,9 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { // Fallback: clear loading when events flow even if no root // session.created was replayed (e.g. CLI snapshot failure). store.set(isLoadingAtom, false); + if (activeSessionType === 'cli') { + config.onRemoteSessionOpened?.({ kiloSessionId }); + } }, }); session.connect(); @@ -567,12 +579,18 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { } satisfies StoredMessage); try { if (!currentSession) throw new Error('No active session'); + // Snapshot before await — switchSession() can overwrite these while send is in flight. + const sessionType = activeSessionType; + const kiloSessionId = activeSessionId; await currentSession.send({ prompt: payload.prompt, mode: payload.mode, model: payload.model, variant: payload.variant, }); + if (sessionType === 'cli' && kiloSessionId) { + config.onRemoteSessionMessageSent?.({ kiloSessionId }); + } } catch (err) { store.set(optimisticMessageAtom, null); store.set(failedPromptAtom, payload.prompt); @@ -635,6 +653,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { } clearAllAtoms(); activeSessionId = null; + activeSessionType = null; } return { diff --git a/src/lib/cloud-agent-sdk/session.ts b/src/lib/cloud-agent-sdk/session.ts index afa8a5b9c..aa89e7b75 100644 --- a/src/lib/cloud-agent-sdk/session.ts +++ b/src/lib/cloud-agent-sdk/session.ts @@ -42,6 +42,7 @@ type CloudAgentSessionConfig = { ) => void; onPermissionResolved?: (requestId: string) => void; onBranchChanged?: (branch: string) => void; + onResolved?: (resolved: ResolvedSession) => void; onSessionCreated?: (info: SessionInfo) => void; onSessionUpdated?: (info: SessionInfo) => void; onEvent?: (event: NormalizedEvent) => void; @@ -225,6 +226,7 @@ function createCloudAgentSession(config: CloudAgentSessionConfig): CloudAgentSes if (expectedGeneration !== connectGeneration) return; console.log('[cli-debug] resolveAndConnect: resolved=%o', resolved); + config.onResolved?.(resolved); let factory: TransportFactory; try {