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

Telemetry for search & replace (#2001)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Adds end-to-end telemetry for Turbo Edits search & replace and wiring to PostHog. > > - Track `search_replace:fix` on initial dry run and each retry with `attemptNumber`, `success`, `issueCount`, and per-file `errors` (emitted from `chat_stream_handlers.ts`) > - New main helper `sendTelemetryEvent` and IPC channel `telemetry:event` (whitelisted in `preload.ts`); renderer subscribes via `IpcClient.onTelemetryEvent` and forwards with `posthog.capture` > - Improves diagnostics: warns with original and diff when `applySearchReplace` fails in `response_processor.ts` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 56bc3a352a2ab3b1e0100923cb395759229ee645. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Added telemetry for Turbo Edits search & replace, forwarding events from main to renderer and capturing them in PostHog. Tracks success/failure and errors for each fix attempt. - **New Features** - Emit "search_replace:fix" with attemptNumber, success, issueCount, and per-file errors on initial attempt and retries. - Add a sendTelemetryEvent helper in main and a "telemetry:event" IPC channel (whitelisted in preload). - Expose IpcClient.onTelemetryEvent; renderer forwards to PostHog via posthog.capture. <sup>Written for commit 56bc3a352a2ab3b1e0100923cb395759229ee645. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 a121dbc9
...@@ -39,6 +39,7 @@ import { streamTestResponse } from "./testing_chat_handlers"; ...@@ -39,6 +39,7 @@ import { streamTestResponse } from "./testing_chat_handlers";
import { getTestResponse } from "./testing_chat_handlers"; import { getTestResponse } from "./testing_chat_handlers";
import { getModelClient, ModelClient } from "../utils/get_model_client"; import { getModelClient, ModelClient } from "../utils/get_model_client";
import log from "electron-log"; import log from "electron-log";
import { sendTelemetryEvent } from "../utils/telemetry";
import { import {
getSupabaseContext, getSupabaseContext,
getSupabaseClientCode, getSupabaseClientCode,
...@@ -1071,6 +1072,15 @@ This conversation includes one or more image attachments. When the user uploads ...@@ -1071,6 +1072,15 @@ This conversation includes one or more image attachments. When the user uploads
fullResponse, fullResponse,
appPath: getDyadAppPath(updatedChat.app.path), appPath: getDyadAppPath(updatedChat.app.path),
}); });
sendTelemetryEvent("search_replace:fix", {
attemptNumber: 0,
success: issues.length === 0,
issueCount: issues.length,
errors: issues.map((i) => ({
filePath: i.filePath,
error: i.error,
})),
});
let searchReplaceFixAttempts = 0; let searchReplaceFixAttempts = 0;
const originalFullResponse = fullResponse; const originalFullResponse = fullResponse;
...@@ -1141,6 +1151,16 @@ ${formattedSearchReplaceIssues}`, ...@@ -1141,6 +1151,16 @@ ${formattedSearchReplaceIssues}`,
fullResponse: result.incrementalResponse, fullResponse: result.incrementalResponse,
appPath: getDyadAppPath(updatedChat.app.path), appPath: getDyadAppPath(updatedChat.app.path),
}); });
sendTelemetryEvent("search_replace:fix", {
attemptNumber: searchReplaceFixAttempts,
success: issues.length === 0,
issueCount: issues.length,
errors: issues.map((i) => ({
filePath: i.filePath,
error: i.error,
})),
});
} }
} }
......
...@@ -76,6 +76,7 @@ import type { ...@@ -76,6 +76,7 @@ import type {
SetAgentToolConsentParams, SetAgentToolConsentParams,
AgentToolConsentRequestPayload, AgentToolConsentRequestPayload,
AgentToolConsentResponseParams, AgentToolConsentResponseParams,
TelemetryEventPayload,
} from "./ipc_types"; } from "./ipc_types";
import type { ConsoleEntry } from "../atoms/appAtoms"; import type { ConsoleEntry } from "../atoms/appAtoms";
import type { Template } from "../shared/templates"; import type { Template } from "../shared/templates";
...@@ -132,6 +133,7 @@ export class IpcClient { ...@@ -132,6 +133,7 @@ export class IpcClient {
>; >;
private mcpConsentHandlers: Map<string, (payload: any) => void>; private mcpConsentHandlers: Map<string, (payload: any) => void>;
private agentConsentHandlers: Map<string, (payload: any) => void>; private agentConsentHandlers: Map<string, (payload: any) => void>;
private telemetryEventHandlers: Set<(payload: TelemetryEventPayload) => void>;
// Global handlers called for any chat stream completion (used for cleanup) // Global handlers called for any chat stream completion (used for cleanup)
private globalChatStreamEndHandlers: Set<(chatId: number) => void>; private globalChatStreamEndHandlers: Set<(chatId: number) => void>;
private constructor() { private constructor() {
...@@ -141,6 +143,7 @@ export class IpcClient { ...@@ -141,6 +143,7 @@ export class IpcClient {
this.helpStreams = new Map(); this.helpStreams = new Map();
this.mcpConsentHandlers = new Map(); this.mcpConsentHandlers = new Map();
this.agentConsentHandlers = new Map(); this.agentConsentHandlers = new Map();
this.telemetryEventHandlers = new Set();
this.globalChatStreamEndHandlers = new Set(); this.globalChatStreamEndHandlers = new Set();
// Set up listeners for stream events // Set up listeners for stream events
this.ipcRenderer.on("chat:response:chunk", (data) => { this.ipcRenderer.on("chat:response:chunk", (data) => {
...@@ -288,6 +291,15 @@ export class IpcClient { ...@@ -288,6 +291,15 @@ export class IpcClient {
const handler = this.agentConsentHandlers.get("consent"); const handler = this.agentConsentHandlers.get("consent");
if (handler) handler(payload); if (handler) handler(payload);
}); });
// Telemetry events from main to renderer
this.ipcRenderer.on("telemetry:event", (payload) => {
if (payload && typeof payload === "object" && "eventName" in payload) {
for (const handler of this.telemetryEventHandlers) {
handler(payload as TelemetryEventPayload);
}
}
});
} }
public static getInstance(): IpcClient { public static getInstance(): IpcClient {
...@@ -970,6 +982,20 @@ export class IpcClient { ...@@ -970,6 +982,20 @@ export class IpcClient {
}; };
} }
/**
* Subscribe to telemetry events from the main process.
* Used to forward events to PostHog in the renderer.
* @returns Unsubscribe function
*/
public onTelemetryEvent(
handler: (payload: TelemetryEventPayload) => void,
): () => void {
this.telemetryEventHandlers.add(handler);
return () => {
this.telemetryEventHandlers.delete(handler);
};
}
// Get proposal details // Get proposal details
public async getProposal(chatId: number): Promise<ProposalResult | null> { public async getProposal(chatId: number): Promise<ProposalResult | null> {
try { try {
......
...@@ -649,3 +649,8 @@ export interface AgentToolConsentResponseParams { ...@@ -649,3 +649,8 @@ export interface AgentToolConsentResponseParams {
// ============================================================================ // ============================================================================
export type AgentToolConsent = "ask" | "always"; export type AgentToolConsent = "ask" | "always";
export interface TelemetryEventPayload {
eventName: string;
properties?: Record<string, unknown>;
}
...@@ -79,6 +79,9 @@ export async function dryRunSearchReplace({ ...@@ -79,6 +79,9 @@ export async function dryRunSearchReplace({
error: error:
"Unable to apply search-replace to file because: " + result.error, "Unable to apply search-replace to file because: " + result.error,
}); });
logger.warn(
`Unable to apply search-replace to file ${filePath} because: ${result.error}. Original content:\n${original}\n Diff content:\n${tag.content}`,
);
continue; continue;
} }
} catch (error) { } catch (error) {
......
import { BrowserWindow } from "electron";
import log from "electron-log";
import { TelemetryEventPayload } from "../ipc_types";
const logger = log.scope("telemetry");
/**
* Sends a telemetry event from the main process to the renderer,
* where PostHog can capture it.
*/
export function sendTelemetryEvent(
eventName: string,
properties?: Record<string, unknown>,
): void {
try {
const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) {
windows[0].webContents.send("telemetry:event", {
eventName,
properties,
} satisfies TelemetryEventPayload);
}
} catch (error) {
logger.warn("Error sending telemetry event:", error);
}
}
...@@ -168,6 +168,8 @@ const validReceiveChannels = [ ...@@ -168,6 +168,8 @@ const validReceiveChannels = [
"mcp:tool-consent-request", "mcp:tool-consent-request",
// Agent tool consent request from main to renderer // Agent tool consent request from main to renderer
"agent-tool:consent-request", "agent-tool:consent-request",
// Telemetry events from main to renderer
"telemetry:event",
] as const; ] as const;
type ValidInvokeChannel = (typeof validInvokeChannels)[number]; type ValidInvokeChannel = (typeof validInvokeChannels)[number];
......
...@@ -157,6 +157,15 @@ function App() { ...@@ -157,6 +157,15 @@ function App() {
return () => unsubscribe(); return () => unsubscribe();
}, [setPendingAgentConsents]); }, [setPendingAgentConsents]);
// Forward telemetry events from main process to PostHog
useEffect(() => {
const ipc = IpcClient.getInstance();
const unsubscribe = ipc.onTelemetryEvent(({ eventName, properties }) => {
posthog.capture(eventName, properties);
});
return () => unsubscribe();
}, []);
return <RouterProvider router={router} />; return <RouterProvider router={router} />;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论