diff --git a/src/renderer/src/components/sidebar/AddRepoDialog.tsx b/src/renderer/src/components/sidebar/AddRepoDialog.tsx index ef5dc4bfec1..7f4d6f6fb77 100644 --- a/src/renderer/src/components/sidebar/AddRepoDialog.tsx +++ b/src/renderer/src/components/sidebar/AddRepoDialog.tsx @@ -297,7 +297,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() { step !== 'setup' || !repoId || !existingWorkspaceTelemetry || - !shouldTrackAddRepoExistingWorkspacesDetected(existingWorkspaceSource) || + !shouldTrackAddRepoExistingWorkspacesDetected(existingWorkspaceTelemetry) || detectedTelemetryTrackedRef.current.has(repoId) ) { return diff --git a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts index 0f000baf827..5ade8cfa3a9 100644 --- a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts +++ b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts @@ -63,14 +63,57 @@ describe('add repo existing workspace telemetry', () => { expect(JSON.stringify(payload)).not.toContain('Feature With User Text') }) - it('omits empty detection payloads and filters new project flows', () => { + it('tracks detection only for imported linked workspaces', () => { expect(buildAddRepoExistingWorkspacesTelemetry(null, [worktree({})])).toBeNull() expect(buildAddRepoExistingWorkspacesTelemetry('local_folder_picker', [])).toBeNull() - expect(shouldTrackAddRepoExistingWorkspacesDetected('local_folder_picker')).toBe(true) - expect(shouldTrackAddRepoExistingWorkspacesDetected('runtime_server_path')).toBe(true) - expect(shouldTrackAddRepoExistingWorkspacesDetected('ssh_remote_path')).toBe(true) - expect(shouldTrackAddRepoExistingWorkspacesDetected('clone_url')).toBe(false) - expect(shouldTrackAddRepoExistingWorkspacesDetected('create_project')).toBe(false) + const mainOnlyPayload = buildAddRepoExistingWorkspacesTelemetry('local_folder_picker', [ + worktree({}) + ]) + expect(mainOnlyPayload?.existing_linked_workspace_count).toBe(0) + expect(shouldTrackAddRepoExistingWorkspacesDetected(mainOnlyPayload)).toBe(false) + + const importedLocalPayload = buildAddRepoExistingWorkspacesTelemetry('local_folder_picker', [ + worktree({}), + worktree({ id: 'repo::/repo-existing', path: '/repo-existing', isMainWorktree: false }) + ]) + const importedRemotePayload = buildAddRepoExistingWorkspacesTelemetry('ssh_remote_path', [ + worktree({}), + worktree({ id: 'repo::/remote-existing', path: '/remote-existing', isMainWorktree: false }) + ]) + const clonePayload = buildAddRepoExistingWorkspacesTelemetry('clone_url', [ + worktree({}), + worktree({ id: 'repo::/clone-existing', path: '/clone-existing', isMainWorktree: false }) + ]) + const createPayload = buildAddRepoExistingWorkspacesTelemetry('create_project', [ + worktree({}), + worktree({ id: 'repo::/create-existing', path: '/create-existing', isMainWorktree: false }) + ]) + + expect(shouldTrackAddRepoExistingWorkspacesDetected(importedLocalPayload)).toBe(true) + expect(shouldTrackAddRepoExistingWorkspacesDetected(importedRemotePayload)).toBe(true) + expect(shouldTrackAddRepoExistingWorkspacesDetected(clonePayload)).toBe(false) + expect(shouldTrackAddRepoExistingWorkspacesDetected(createPayload)).toBe(false) + }) + + it('derives linked and detached counts before clamping reported values', () => { + const mainOnlyPayload = buildAddRepoExistingWorkspacesTelemetry( + 'local_folder_picker', + Array.from({ length: 60 }, (_, index) => + worktree({ + id: `repo::/repo-main-${index}`, + path: `/repo-main-${index}`, + isMainWorktree: true + }) + ) + ) + + expect(mainOnlyPayload).toMatchObject({ + existing_workspace_count: 50, + existing_linked_workspace_count: 0, + main_workspace_count: 50, + detached_workspace_count: 0 + }) + expect(shouldTrackAddRepoExistingWorkspacesDetected(mainOnlyPayload)).toBe(false) }) }) diff --git a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts index a5b46ea1886..f1511e2bf5f 100644 --- a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts +++ b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts @@ -42,37 +42,38 @@ export function buildAddRepoExistingWorkspacesTelemetry( return null } - const existing_workspace_count = countWorkspaces(worktrees.length) - const main_workspace_count = countWorkspaces( - worktrees.filter((worktree) => worktree.isMainWorktree).length - ) - const branch_named_workspace_count = countWorkspaces( - worktrees.filter((worktree) => Boolean(branchDisplayName(worktree))).length - ) - const sparse_workspace_count = countWorkspaces( - worktrees.filter((worktree) => worktree.isSparse === true).length - ) + const mainWorkspaceCount = worktrees.filter((worktree) => worktree.isMainWorktree).length + const branchNamedWorkspaceCount = worktrees.filter((worktree) => + Boolean(branchDisplayName(worktree)) + ).length + const sparseWorkspaceCount = worktrees.filter((worktree) => worktree.isSparse === true).length return { source, - existing_workspace_count, - existing_linked_workspace_count: countWorkspaces(worktrees.length - main_workspace_count), - main_workspace_count, - branch_named_workspace_count, - detached_workspace_count: countWorkspaces(worktrees.length - branch_named_workspace_count), + existing_workspace_count: countWorkspaces(worktrees.length), + existing_linked_workspace_count: countWorkspaces(worktrees.length - mainWorkspaceCount), + main_workspace_count: countWorkspaces(mainWorkspaceCount), + branch_named_workspace_count: countWorkspaces(branchNamedWorkspaceCount), + detached_workspace_count: countWorkspaces(worktrees.length - branchNamedWorkspaceCount), custom_named_workspace_count: countWorkspaces(worktrees.filter(isCustomDisplayName).length), - sparse_workspace_count + sparse_workspace_count: countWorkspaces(sparseWorkspaceCount) } } export function shouldTrackAddRepoExistingWorkspacesDetected( - source: AddRepoExistingWorkspaceSource | null + payload: ExistingWorkspacesDetectedProps | null ): boolean { + // Track the import/discovery signal, not mere setup-modal exposure: the main + // checkout is always a worktree, but only non-main worktrees imply migration. + if (!payload || payload.existing_linked_workspace_count === 0) { + return false + } + // Clone/create produce a new project during this flow, so their setup step is // not evidence of a pre-existing workspace migration opportunity. return ( - source === 'local_folder_picker' || - source === 'runtime_server_path' || - source === 'ssh_remote_path' + payload.source === 'local_folder_picker' || + payload.source === 'runtime_server_path' || + payload.source === 'ssh_remote_path' ) } diff --git a/src/shared/add-repo-existing-workspaces-telemetry.test.ts b/src/shared/add-repo-existing-workspaces-telemetry.test.ts index 3f080fd7f11..3804cec5a8b 100644 --- a/src/shared/add-repo-existing-workspaces-telemetry.test.ts +++ b/src/shared/add-repo-existing-workspaces-telemetry.test.ts @@ -27,19 +27,22 @@ describe('add_repo_existing_workspaces_detected schema', () => { detached_workspace_count: 0, custom_named_workspace_count: 0, sparse_workspace_count: 0, + nth_repo_added: 1, workspace_names: ['secret-customer-branch'] }) expect(parsed.success).toBe(false) }) - it('accepts setup actions with bounded existing-workspace context', () => { - const parsed = eventSchemas.add_repo_setup_step_action.safeParse({ - action: 'open_existing', - source: 'ssh_remote_path', - existing_workspace_count: 4, - existing_linked_workspace_count: 3, - nth_repo_added: 1 - }) - expect(parsed.success).toBe(true) + it('accepts setup start choices with bounded existing-workspace context', () => { + for (const action of ['open_existing', 'create_worktree'] as const) { + const parsed = eventSchemas.add_repo_setup_step_action.safeParse({ + action, + source: 'ssh_remote_path', + existing_workspace_count: 4, + existing_linked_workspace_count: 3, + nth_repo_added: 1 + }) + expect(parsed.success).toBe(true) + } }) })