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";
import { getTestResponse } from "./testing_chat_handlers";
import { getModelClient, ModelClient } from "../utils/get_model_client";
import log from "electron-log";
import { sendTelemetryEvent } from "../utils/telemetry";
import {
getSupabaseContext,
getSupabaseClientCode,
......@@ -1071,6 +1072,15 @@ This conversation includes one or more image attachments. When the user uploads
fullResponse,
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;
const originalFullResponse = fullResponse;
......@@ -1141,6 +1151,16 @@ ${formattedSearchReplaceIssues}`,
fullResponse: result.incrementalResponse,
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 {
SetAgentToolConsentParams,
AgentToolConsentRequestPayload,
AgentToolConsentResponseParams,
TelemetryEventPayload,
} from "./ipc_types";
import type { ConsoleEntry } from "../atoms/appAtoms";
import type { Template } from "../shared/templates";
......@@ -132,6 +133,7 @@ export class IpcClient {
>;
private mcpConsentHandlers: 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)
private globalChatStreamEndHandlers: Set<(chatId: number) => void>;
private constructor() {
......@@ -141,6 +143,7 @@ export class IpcClient {
this.helpStreams = new Map();
this.mcpConsentHandlers = new Map();
this.agentConsentHandlers = new Map();
this.telemetryEventHandlers = new Set();
this.globalChatStreamEndHandlers = new Set();
// Set up listeners for stream events
this.ipcRenderer.on("chat:response:chunk", (data) => {
......@@ -288,6 +291,15 @@ export class IpcClient {
const handler = this.agentConsentHandlers.get("consent");
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 {
......@@ -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
public async getProposal(chatId: number): Promise<ProposalResult | null> {
try {
......
......@@ -649,3 +649,8 @@ export interface AgentToolConsentResponseParams {
// ============================================================================
export type AgentToolConsent = "ask" | "always";
export interface TelemetryEventPayload {
eventName: string;
properties?: Record<string, unknown>;
}
......@@ -79,6 +79,9 @@ export async function dryRunSearchReplace({
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;
}
} 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 = [
"mcp:tool-consent-request",
// Agent tool consent request from main to renderer
"agent-tool:consent-request",
// Telemetry events from main to renderer
"telemetry:event",
] as const;
type ValidInvokeChannel = (typeof validInvokeChannels)[number];
......
......@@ -157,6 +157,15 @@ function App() {
return () => unsubscribe();
}, [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} />;
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论