From b5a4073013f065792e4feb52ea128b3fcc79f607 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Fri, 8 May 2026 09:46:32 -0700 Subject: [PATCH] feat: track task creation with workspace mode and worktree file usage analytics Generated-By: PostHog Code Task-Id: f5278728-a8c6-46c7-a923-150789eee599 --- .../src/main/services/workspace/schemas.ts | 9 +++ .../src/main/services/workspace/service.ts | 32 +++++++++++ apps/code/src/main/trpc/routers/workspace.ts | 9 +++ .../task-detail/hooks/useTaskCreation.ts | 55 +++++++++++++++++++ .../renderer/features/tasks/hooks/useTasks.ts | 11 ---- apps/code/src/shared/types/analytics.ts | 13 +++++ 6 files changed, 118 insertions(+), 11 deletions(-) diff --git a/apps/code/src/main/services/workspace/schemas.ts b/apps/code/src/main/services/workspace/schemas.ts index 72137ad48..79cafc1c0 100644 --- a/apps/code/src/main/services/workspace/schemas.ts +++ b/apps/code/src/main/services/workspace/schemas.ts @@ -169,6 +169,15 @@ export const listGitWorktreesInput = z.object({ mainRepoPath: z.string(), }); +export const getWorktreeFileUsageInput = z.object({ + mainRepoPath: z.string(), +}); + +export const getWorktreeFileUsageOutput = z.object({ + usesWorktreeLink: z.boolean(), + usesWorktreeInclude: z.boolean(), +}); + export const gitWorktreeEntrySchema = z.object({ worktreePath: z.string(), head: z.string(), diff --git a/apps/code/src/main/services/workspace/service.ts b/apps/code/src/main/services/workspace/service.ts index dea586081..10ddfc363 100644 --- a/apps/code/src/main/services/workspace/service.ts +++ b/apps/code/src/main/services/workspace/service.ts @@ -59,6 +59,28 @@ type TaskAssociation = branchName: string | null; }; +/** + * True if a worktree exclude file (.worktreelink / .worktreeinclude) exists and has at least + * one non-empty, non-comment entry. + */ +async function hasExcludeFileEntries( + mainRepoPath: string, + fileName: string, +): Promise { + try { + const contents = await fsPromises.readFile( + path.join(mainRepoPath, fileName), + "utf8", + ); + return contents.split("\n").some((line) => { + const trimmed = line.trim(); + return trimmed.length > 0 && !trimmed.startsWith("#"); + }); + } catch { + return false; + } +} + async function hasAnyFiles(repoPath: string): Promise { try { const entries = await fsPromises.readdir(repoPath); @@ -1089,6 +1111,16 @@ export class WorkspaceService extends TypedEventEmitter }); } + async getWorktreeFileUsage( + mainRepoPath: string, + ): Promise<{ usesWorktreeLink: boolean; usesWorktreeInclude: boolean }> { + const [usesWorktreeLink, usesWorktreeInclude] = await Promise.all([ + hasExcludeFileEntries(mainRepoPath, ".worktreelink"), + hasExcludeFileEntries(mainRepoPath, ".worktreeinclude"), + ]); + return { usesWorktreeLink, usesWorktreeInclude }; + } + async getWorktreeSize(worktreePath: string): Promise<{ sizeBytes: number }> { try { const { stdout } = await execFileAsync("du", ["-s", worktreePath]); diff --git a/apps/code/src/main/trpc/routers/workspace.ts b/apps/code/src/main/trpc/routers/workspace.ts index ca1085e3e..97f44604f 100644 --- a/apps/code/src/main/trpc/routers/workspace.ts +++ b/apps/code/src/main/trpc/routers/workspace.ts @@ -16,6 +16,8 @@ import { getTaskTimestampsOutput, getWorkspaceInfoInput, getWorkspaceInfoOutput, + getWorktreeFileUsageInput, + getWorktreeFileUsageOutput, getWorktreeSizeInput, getWorktreeSizeOutput, getWorktreeTasksInput, @@ -106,6 +108,13 @@ export const workspaceRouter = router({ .output(getWorktreeSizeOutput) .query(({ input }) => getService().getWorktreeSize(input.worktreePath)), + getWorktreeFileUsage: publicProcedure + .input(getWorktreeFileUsageInput) + .output(getWorktreeFileUsageOutput) + .query(({ input }) => + getService().getWorktreeFileUsage(input.mainRepoPath), + ), + deleteWorktree: publicProcedure .input(deleteWorktreeInput) .mutation(({ input }) => diff --git a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts index e7f36507d..6169c42ce 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts @@ -14,9 +14,12 @@ import { useConnectivity } from "@hooks/useConnectivity"; import type { WorkspaceMode } from "@main/services/workspace/schemas"; import { get } from "@renderer/di/container"; import { RENDERER_TOKENS } from "@renderer/di/tokens"; +import { trpcClient } from "@renderer/trpc/client"; import { toast } from "@renderer/utils/toast"; import type { ExecutionMode, Task } from "@shared/types"; +import { ANALYTICS_EVENTS } from "@shared/types/analytics"; import { useNavigationStore } from "@stores/navigationStore"; +import { track } from "@utils/analytics"; import { logger } from "@utils/logger"; import { useCallback, useState } from "react"; import type { TaskCreationInput, TaskService } from "../service/service"; @@ -104,6 +107,54 @@ function prepareTaskInput( }; } +async function trackTaskCreated( + input: TaskCreationInput, + selectedDirectory: string, +): Promise { + try { + const workspaceMode = input.workspaceMode ?? "local"; + + let usesWorktreeLink: boolean | undefined; + let usesWorktreeInclude: boolean | undefined; + if (workspaceMode === "worktree" && selectedDirectory) { + try { + const usage = await trpcClient.workspace.getWorktreeFileUsage.query({ + mainRepoPath: selectedDirectory, + }); + usesWorktreeLink = usage.usesWorktreeLink; + usesWorktreeInclude = usage.usesWorktreeInclude; + } catch (error) { + log.warn("Failed to read worktree file usage for analytics", { + error, + }); + } + } + + track(ANALYTICS_EVENTS.TASK_CREATED, { + auto_run: !!input.executionMode, + created_from: "command-menu", + repository_provider: input.repository ? "github" : "none", + workspace_mode: workspaceMode, + has_branch: !!input.branch, + has_environment_setup: + workspaceMode === "worktree" ? !!input.environmentId : undefined, + has_sandbox_environment: + workspaceMode === "cloud" ? !!input.sandboxEnvironmentId : undefined, + cloud_run_source: + workspaceMode === "cloud" + ? (input.cloudRunSource ?? "manual") + : undefined, + cloud_pr_authorship_mode: + workspaceMode === "cloud" ? input.cloudPrAuthorshipMode : undefined, + uses_worktree_link: usesWorktreeLink, + uses_worktree_include: usesWorktreeInclude, + adapter: input.adapter, + }); + } catch (error) { + log.warn("Failed to track Task created event", { error }); + } +} + function getErrorTitle(failedStep: string): string { const titles: Record = { repo_detection: "Failed to detect repository", @@ -202,6 +253,10 @@ export function useTaskCreation({ editor.clear(); }); + if (result.success) { + void trackTaskCreated(input, selectedDirectory); + } + if (!result.success) { const title = getErrorTitle(result.failedStep); toast.error(title, { description: result.error }); diff --git a/apps/code/src/renderer/features/tasks/hooks/useTasks.ts b/apps/code/src/renderer/features/tasks/hooks/useTasks.ts index dfd203e3c..07c5f7e6b 100644 --- a/apps/code/src/renderer/features/tasks/hooks/useTasks.ts +++ b/apps/code/src/renderer/features/tasks/hooks/useTasks.ts @@ -8,9 +8,7 @@ import { useFocusStore } from "@renderer/stores/focusStore"; import { useNavigationStore } from "@renderer/stores/navigationStore"; import { trpcClient } from "@renderer/trpc/client"; import type { Task } from "@shared/types"; -import { ANALYTICS_EVENTS } from "@shared/types/analytics"; import { keepPreviousData, useQueryClient } from "@tanstack/react-query"; -import { track } from "@utils/analytics"; import { logger } from "@utils/logger"; import { useCallback } from "react"; @@ -111,15 +109,6 @@ export function useCreateTask() { repository, github_integration, }) as unknown as Promise, - { - onSuccess: (_task, variables) => { - track(ANALYTICS_EVENTS.TASK_CREATED, { - auto_run: false, - created_from: variables.createdFrom || "cli", - repository_provider: variables.repository ? "github" : "none", - }); - }, - }, ); return { ...mutation, invalidateTasks }; diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 2c7416503..9c1a54791 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -47,6 +47,19 @@ export interface TaskCreateProperties { auto_run: boolean; created_from: TaskCreatedFrom; repository_provider?: RepositoryProvider; + workspace_mode?: "local" | "worktree" | "cloud"; + has_branch?: boolean; + /** Worktree mode: a project environment with a setup script was selected */ + has_environment_setup?: boolean; + /** Cloud mode: a sandbox environment was selected */ + has_sandbox_environment?: boolean; + cloud_run_source?: "manual" | "signal_report"; + cloud_pr_authorship_mode?: "user" | "bot"; + /** Worktree mode: repo has a non-empty .worktreelink file */ + uses_worktree_link?: boolean; + /** Worktree mode: repo has a non-empty .worktreeinclude file */ + uses_worktree_include?: boolean; + adapter?: "claude" | "codex"; } export interface TaskViewProperties {