Unverified 提交 20b95024 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add telemetry for local-agent search-replace operations (#2371)

## Summary - Add `local_agent:search_replace:success` and `local_agent:search_replace:failure` telemetry events to track search-replace outcomes in local-agent mode - Add `local_agent:file_edit_retry` telemetry to detect when multiple edit tool types (write_file, edit_file, search_replace) are used on the same file, indicating retry/fallback behavior - Add `FileEditTracker` to `AgentContext` to track tool usage per file during an agent session ## Test plan - [x] TypeScript type checks pass - [x] Linter passes - [x] All 654 unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) #skip-bugbot <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2371"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds telemetry to local-agent to track search-replace success/failure and when multiple edit tools touch the same file. Introduces a FileEditTracker in AgentContext to record write_file, edit_file, and search_replace usage per file. - **New Features** - Emit local_agent:search_replace:success and local_agent:search_replace:failure (includes filePath and error on failure). - Emit local_agent:file_edit_retry when a file uses 2+ different edit tools; includes per-tool counts. - Track usage via FileEditTracker on AgentContext with a helper that records tool calls. - **Migration** - Update any AgentContext mocks/constructors to include fileEditTracker: {}. <sup>Written for commit f8345128b1d29b555c4c787df7103a6cae98373b. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 477015ef
...@@ -48,7 +48,9 @@ import { ...@@ -48,7 +48,9 @@ import {
escapeXmlAttr, escapeXmlAttr,
escapeXmlContent, escapeXmlContent,
UserMessageContentPart, UserMessageContentPart,
FileEditTracker,
} from "./tools/types"; } from "./tools/types";
import { sendTelemetryEvent } from "@/ipc/utils/telemetry";
import { import {
prepareStepMessages, prepareStepMessages,
type InjectedMessage, type InjectedMessage,
...@@ -176,6 +178,7 @@ export async function handleLocalAgentStream( ...@@ -176,6 +178,7 @@ export async function handleLocalAgentStream(
); );
// Build tool execute context // Build tool execute context
const fileEditTracker: FileEditTracker = Object.create(null);
const ctx: AgentContext = { const ctx: AgentContext = {
event, event,
appId: chat.app.id, appId: chat.app.id,
...@@ -187,6 +190,7 @@ export async function handleLocalAgentStream( ...@@ -187,6 +190,7 @@ export async function handleLocalAgentStream(
isSharedModulesChanged: false, isSharedModulesChanged: false,
todos: [], todos: [],
dyadRequestId, dyadRequestId,
fileEditTracker,
onXmlStream: (accumulatedXml: string) => { onXmlStream: (accumulatedXml: string) => {
// Stream accumulated XML to UI without persisting // Stream accumulated XML to UI without persisting
streamingPreview = accumulatedXml; streamingPreview = accumulatedXml;
...@@ -452,6 +456,17 @@ export async function handleLocalAgentStream( ...@@ -452,6 +456,17 @@ export async function handleLocalAgentStream(
.set({ approvalState: "approved" }) .set({ approvalState: "approved" })
.where(eq(messages.id, placeholderMessageId)); .where(eq(messages.id, placeholderMessageId));
// Send telemetry for files with multiple edit tool types
for (const [filePath, counts] of Object.entries(fileEditTracker)) {
const toolsUsed = Object.entries(counts).filter(([, count]) => count > 0);
if (toolsUsed.length >= 2) {
sendTelemetryEvent("local_agent:file_edit_retry", {
filePath,
...counts,
});
}
}
// Send completion // Send completion
safeSend(event.sender, "chat:response:end", { safeSend(event.sender, "chat:response:end", {
chatId: req.chatId, chatId: req.chatId,
......
...@@ -34,6 +34,8 @@ import { ...@@ -34,6 +34,8 @@ import {
type ToolDefinition, type ToolDefinition,
type AgentContext, type AgentContext,
type ToolResult, type ToolResult,
type FileEditToolName,
FILE_EDIT_TOOL_NAMES,
} from "./tools/types"; } from "./tools/types";
import { AgentToolConsent } from "@/lib/schemas"; import { AgentToolConsent } from "@/lib/schemas";
import { getSupabaseClientCode } from "@/supabase_admin/supabase_context"; import { getSupabaseClientCode } from "@/supabase_admin/supabase_context";
...@@ -263,6 +265,33 @@ export interface BuildAgentToolSetOptions { ...@@ -263,6 +265,33 @@ export interface BuildAgentToolSetOptions {
readOnly?: boolean; readOnly?: boolean;
} }
const FILE_EDIT_TOOLS: Set<FileEditToolName> = new Set(FILE_EDIT_TOOL_NAMES);
/**
* Track file edit tool usage for telemetry
*/
function trackFileEditTool(
ctx: AgentContext,
toolName: string,
args: { file_path?: string; path?: string },
): void {
if (!FILE_EDIT_TOOLS.has(toolName as FileEditToolName)) {
return;
}
const filePath = args.file_path ?? args.path;
if (!filePath) {
return;
}
if (!ctx.fileEditTracker[filePath]) {
ctx.fileEditTracker[filePath] = {
write_file: 0,
edit_file: 0,
search_replace: 0,
};
}
ctx.fileEditTracker[filePath][toolName as FileEditToolName]++;
}
/** /**
* Build ToolSet for AI SDK from tool definitions * Build ToolSet for AI SDK from tool definitions
*/ */
...@@ -304,7 +333,12 @@ export function buildAgentToolSet( ...@@ -304,7 +333,12 @@ export function buildAgentToolSet(
throw new Error(`User denied permission for ${tool.name}`); throw new Error(`User denied permission for ${tool.name}`);
} }
// Track file edit tool usage before execution to capture all attempts
// (including failures) for retry/fallback telemetry
trackFileEditTool(ctx, tool.name, processedArgs);
const result = await tool.execute(processedArgs, ctx); const result = await tool.execute(processedArgs, ctx);
return convertToolResultForAiSdk(result); return convertToolResultForAiSdk(result);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
......
...@@ -47,6 +47,7 @@ describe("searchReplaceTool", () => { ...@@ -47,6 +47,7 @@ describe("searchReplaceTool", () => {
isSharedModulesChanged: false, isSharedModulesChanged: false,
todos: [], todos: [],
dyadRequestId: "test-request", dyadRequestId: "test-request",
fileEditTracker: {},
onXmlStream: vi.fn(), onXmlStream: vi.fn(),
onXmlComplete: vi.fn(), onXmlComplete: vi.fn(),
requireConsent: vi.fn().mockResolvedValue(true), requireConsent: vi.fn().mockResolvedValue(true),
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
isServerFunction, isServerFunction,
isSharedServerModule, isSharedServerModule,
} from "@/supabase_admin/supabase_utils"; } from "@/supabase_admin/supabase_utils";
import { sendTelemetryEvent } from "@/ipc/utils/telemetry";
const logger = log.scope("search_replace"); const logger = log.scope("search_replace");
...@@ -114,6 +115,10 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL: ...@@ -114,6 +115,10 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
const result = applySearchReplace(original, operations); const result = applySearchReplace(original, operations);
if (!result.success || typeof result.content !== "string") { if (!result.success || typeof result.content !== "string") {
sendTelemetryEvent("local_agent:search_replace:failure", {
filePath: args.file_path,
error: result.error ?? "unknown",
});
throw new Error( throw new Error(
`Failed to apply search-replace: ${result.error ?? "unknown"}`, `Failed to apply search-replace: ${result.error ?? "unknown"}`,
); );
...@@ -121,6 +126,9 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL: ...@@ -121,6 +126,9 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
await fs.promises.writeFile(fullFilePath, result.content); await fs.promises.writeFile(fullFilePath, result.content);
logger.log(`Successfully applied search-replace to: ${fullFilePath}`); logger.log(`Successfully applied search-replace to: ${fullFilePath}`);
sendTelemetryEvent("local_agent:search_replace:success", {
filePath: args.file_path,
});
// Deploy Supabase function if applicable // Deploy Supabase function if applicable
if ( if (
......
...@@ -26,6 +26,21 @@ export { ...@@ -26,6 +26,21 @@ export {
// Re-export AgentTodo as Todo for backwards compatibility within this module // Re-export AgentTodo as Todo for backwards compatibility within this module
export type Todo = AgentTodo; export type Todo = AgentTodo;
/** Tracks which file-editing tools were used on each file path */
export const FILE_EDIT_TOOL_NAMES = [
"write_file",
"edit_file",
"search_replace",
] as const;
export type FileEditToolName = (typeof FILE_EDIT_TOOL_NAMES)[number];
export interface FileEditTracker {
[filePath: string]: {
write_file: number;
edit_file: number;
search_replace: number;
};
}
export interface AgentContext { export interface AgentContext {
event: IpcMainInvokeEvent; event: IpcMainInvokeEvent;
appId: number; appId: number;
...@@ -40,6 +55,8 @@ export interface AgentContext { ...@@ -40,6 +55,8 @@ export interface AgentContext {
todos: Todo[]; todos: Todo[];
/** Request ID for tracking requests to the Dyad engine */ /** Request ID for tracking requests to the Dyad engine */
dyadRequestId: string; dyadRequestId: string;
/** Tracks file edit tool usage per file for telemetry */
fileEditTracker: FileEditTracker;
/** /**
* Streams accumulated XML to UI without persisting to DB (for live preview). * Streams accumulated XML to UI without persisting to DB (for live preview).
* Call this repeatedly with the full accumulated XML so far. * Call this repeatedly with the full accumulated XML so far.
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论