Skip to content

Commit 9ca0fb1

Browse files
committed
feat(metrics): broad ui/feature usage instrumentation
Adds new EVENT_TYPE entries (AI = "ai", TERMINAL = "term") and wires metadata-only metrics across the editor's main feature surfaces. All labels are stable enums; no user content (filenames, prompt text, shell input/output, attached file paths) is recorded. Coverage: - src/view/CentralControlBar.js Click metrics for every CCB icon — UI/ccb/{undo,redo,save, designOn,designOff,sidebar,file}. - src/view/SidebarTabs.js Click metric for the sidebar nav tab bar — UI/navTab/<tabId> (e.g. "ai", "files"). Programmatic setActiveTab calls are NOT metric'd; only user clicks. - src/extensionsIntegrated/Phoenix-live-preview/main.js lp-edit/modeBtn/{toEdit,toPrev} on the inline edit-mode toggle button (previewModeLivePreviewButton). The companion designModeBtn already had a metric. - src/extensionsIntegrated/Terminal/main.js Terminal feature usage: term/panel/open — panel becomes visible term/new/<shell> — new tab created, shell family bucketed (bash, zsh, fish, pwsh, cmd, ...) term/tabs/<bucket> — concurrent tab count after creation, bucketed one / LTE4 / LTE9 / GT10 term/pick/<shell> — user picked a default shell in the dropdown term/close/user — terminal closed via UI term/exit/{ok,err} — shell process exited on its own (status bucketed) - src-node/claude-code-agent.js Per-turn token usage console-logs (each SDKAssistantMessage's message.usage) and one rolled-up usage payload per query (the terminal SDKResultMessage). The aggregate is forwarded to the browser via a new aiUsage peer event with usage / modelUsage / total_cost_usd / num_turns / duration_ms / duration_api_ms. - src/utils/Metrics.js EVENT_TYPE.AI and EVENT_TYPE.TERMINAL added so feature areas have their own dimensions instead of overloading UI / PRO.
1 parent 23fd2e7 commit 9ca0fb1

6 files changed

Lines changed: 149 additions & 3 deletions

File tree

src-node/claude-code-agent.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,67 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
13401340
_log("Session:", currentSessionId);
13411341
}
13421342

1343+
// Per-turn token usage: each SDKAssistantMessage carries the
1344+
// wrapped Anthropic API message whose `.usage` reflects what
1345+
// that single turn consumed. Useful for diagnosing runaway
1346+
// loops; logged but not metric'd individually (the result
1347+
// message rolls up the session totals).
1348+
if (message.type === "assistant" &&
1349+
message.message && message.message.usage) {
1350+
const u = message.message.usage;
1351+
_log("Turn usage:",
1352+
"in=" + (u.input_tokens || 0),
1353+
"out=" + (u.output_tokens || 0),
1354+
"cacheRead=" + (u.cache_read_input_tokens || 0),
1355+
"cacheCreate=" + (u.cache_creation_input_tokens || 0),
1356+
message.parent_tool_use_id ? "(subagent)" : "");
1357+
}
1358+
1359+
// Aggregate session usage on the terminal `result` message.
1360+
// The SDK emits exactly one of these per query (success or
1361+
// error_*) with totals across all turns and the per-model
1362+
// breakdown.
1363+
if (message.type === "result") {
1364+
const u = message.usage || {};
1365+
const mu = message.modelUsage || {};
1366+
_log("Result:",
1367+
"turns=" + (message.num_turns || 0),
1368+
"in=" + (u.input_tokens || 0),
1369+
"out=" + (u.output_tokens || 0),
1370+
"cacheRead=" + (u.cache_read_input_tokens || 0),
1371+
"cacheCreate=" + (u.cache_creation_input_tokens || 0),
1372+
"cost=$" + (message.total_cost_usd || 0).toFixed(4),
1373+
"ms=" + (message.duration_ms || 0),
1374+
"apiMs=" + (message.duration_api_ms || 0),
1375+
"subtype=" + message.subtype);
1376+
for (const modelName of Object.keys(mu)) {
1377+
const m = mu[modelName];
1378+
_log("Model usage[" + modelName + "]:",
1379+
"in=" + (m.inputTokens || 0),
1380+
"out=" + (m.outputTokens || 0),
1381+
"cacheRead=" + (m.cacheReadInputTokens || 0),
1382+
"cacheCreate=" + (m.cacheCreationInputTokens || 0),
1383+
"websearch=" + (m.webSearchRequests || 0),
1384+
"cost=$" + (m.costUSD || 0).toFixed(4),
1385+
"ctxWindow=" + (m.contextWindow || 0));
1386+
}
1387+
// Forward to the browser so AIChatPanel can raise metrics.
1388+
// Stuck on its own event so the existing aiComplete handler
1389+
// doesn't have to change shape.
1390+
nodeConnector.triggerPeer("aiUsage", {
1391+
requestId: requestId,
1392+
sessionId: currentSessionId,
1393+
subtype: message.subtype,
1394+
isError: !!message.is_error,
1395+
numTurns: message.num_turns || 0,
1396+
durationMs: message.duration_ms || 0,
1397+
durationApiMs: message.duration_api_ms || 0,
1398+
totalCostUSD: message.total_cost_usd || 0,
1399+
usage: u,
1400+
modelUsage: mu
1401+
});
1402+
}
1403+
13431404
// Handle streaming events
13441405
if (message.type === "stream_event") {
13451406
const event = message.event;

src/extensionsIntegrated/Phoenix-live-preview/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,12 @@ define(function (require, exports, module) {
755755
*/
756756
function _handlePreviewBtnClick() {
757757
const currentMode = LiveDevelopment.getCurrentMode();
758+
// Metric: which direction the user is toggling so we can see
759+
// how often the inline-edit affordance is actually used vs
760+
// simply opened-then-closed.
761+
Metrics.countEvent(Metrics.EVENT_TYPE.LP_EDIT, "modeBtn",
762+
currentMode === LiveDevelopment.CONSTANTS.LIVE_EDIT_MODE
763+
? "toPrev" : "toEdit");
758764
if (currentMode === LiveDevelopment.CONSTANTS.LIVE_EDIT_MODE) {
759765
LiveDevelopment.setMode(LiveDevelopment.CONSTANTS.LIVE_PREVIEW_MODE);
760766
return;

src/extensionsIntegrated/Terminal/main.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ define(function (require, exports, module) {
3333
const WorkspaceManager = require("view/WorkspaceManager");
3434
const ProjectManager = require("project/ProjectManager");
3535
const ExtensionUtils = require("utils/ExtensionUtils");
36+
const Metrics = require("utils/Metrics");
3637
const NodeConnector = require("NodeConnector");
3738
const Mustache = require("thirdparty/mustache/mustache");
3839
const Dialogs = require("widgets/Dialogs");
@@ -167,6 +168,7 @@ define(function (require, exports, module) {
167168
active.focus();
168169
}
169170
_showFocusHintToast();
171+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "panel", "open");
170172
}
171173
});
172174

@@ -199,6 +201,11 @@ define(function (require, exports, module) {
199201
ShellProfiles.setDefaultShell(shell.name);
200202
_populateShellDropdown();
201203
_updateNewTerminalButtonLabel();
204+
// Metric: which shell the user picked from the dropdown
205+
// (default-shell switch). _createNewTerminalWithShell below
206+
// will also raise its own "new" metric for the spawn.
207+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "pick",
208+
_shellMetricLabel(shell.name));
202209
_createNewTerminalWithShell(shell);
203210
});
204211
$shellDropdown.append($item);
@@ -290,11 +297,33 @@ define(function (require, exports, module) {
290297
* @param {Object} shell - Shell profile to use
291298
* @param {string} [cwdOverride] - Optional VFS path to use as cwd instead of project root
292299
*/
300+
/**
301+
* Map an OS shell name (e.g. "powershell.exe", "bash.exe") to a short
302+
* family label so the metrics server's per-event length budget stays
303+
* comfortable. Strips ".exe" and lower-cases; unknown shells fall
304+
* through under "other" (with their lower-cased name shown only in
305+
* the cap'd 8-char form).
306+
*/
307+
function _shellMetricLabel(shellName) {
308+
if (!shellName) { return "unknown"; }
309+
let n = String(shellName).toLowerCase();
310+
if (n.endsWith(".exe")) { n = n.slice(0, -4); }
311+
// pwsh & powershell are functionally the same family for the metric.
312+
if (n === "powershell") { n = "pwsh"; }
313+
// Cap so an unexpected long shell name can't blow the label cell.
314+
if (n.length > 8) { n = n.slice(0, 8); }
315+
return n || "unknown";
316+
}
317+
293318
async function _createNewTerminalWithShell(shell, cwdOverride) {
294319
if (!shell) {
295320
console.error("Terminal: No shell available");
321+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", "noShell");
296322
return;
297323
}
324+
// Metric: a new terminal session was created, keyed by shell family.
325+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new",
326+
_shellMetricLabel(shell.name));
298327

299328
// Get cwd: use override if provided, otherwise fall back to project root
300329
let cwd;
@@ -320,6 +349,16 @@ define(function (require, exports, module) {
320349
// Add to list
321350
terminalInstances.push(instance);
322351

352+
// Tab-count bucket metric — raised only on tab creation so we
353+
// can plot how many concurrent terminal tabs users keep open.
354+
// Buckets: 1 → "one", 2..4 → "LTE4", 5..9 → "LTE9", 10+ → "GT10".
355+
const count = terminalInstances.length;
356+
const tabsBucket = count === 1 ? "one"
357+
: count <= 4 ? "LTE4"
358+
: count <= 9 ? "LTE9"
359+
: "GT10";
360+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "tabs", tabsBucket);
361+
323362
// Activate this terminal (also updates flyout)
324363
_activateTerminal(instance.id);
325364

@@ -394,6 +433,7 @@ define(function (require, exports, module) {
394433
instance.dispose();
395434
terminalInstances.splice(idx, 1);
396435
delete processInfo[id];
436+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "close", "user");
397437

398438
// If we closed the active terminal, activate another
399439
if (activeTerminalId === id) {
@@ -563,6 +603,12 @@ define(function (require, exports, module) {
563603
function _onTerminalProcessExit(id, exitCode) {
564604
delete processInfo[id];
565605
_updateFlyout();
606+
// Metric: terminal process exited on its own (e.g. user typed
607+
// "exit"). Distinct from "user" close above, which records the
608+
// X-button / panel-driven close path. Exit code is bucketed
609+
// ok/err so cardinality stays bounded.
610+
Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "exit",
611+
exitCode === 0 ? "ok" : "err");
566612
}
567613

568614
/**

src/utils/Metrics.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ define(function (require, exports, module) {
129129
GIT: "git",
130130
AUTH: "auth",
131131
PRO: "pro",
132+
AI: "ai",
133+
TERMINAL: "term",
132134
GUIDE: "guide"
133135
};
134136

src/view/CentralControlBar.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ define(function (require, exports, module) {
2525
const Commands = require("command/Commands");
2626
const DocumentManager = require("document/DocumentManager");
2727
const MainViewManager = require("view/MainViewManager");
28+
const Metrics = require("utils/Metrics");
2829
const Strings = require("strings");
2930
const WorkspaceManager = require("view/WorkspaceManager");
3031
const SidebarView = require("project/SidebarView");
@@ -302,20 +303,41 @@ define(function (require, exports, module) {
302303
$btn.find("i").attr("class", isVisible ? "fa-solid fa-angles-left" : "fa-solid fa-angles-right");
303304
}
304305

306+
function _ccbClickMetric(label) {
307+
Metrics.countEvent(Metrics.EVENT_TYPE.UI, "ccb", label);
308+
}
309+
305310
function _wireButtons() {
306-
$("#ccbUndoBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.EDIT_UNDO); });
307-
$("#ccbRedoBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.EDIT_REDO); });
308-
$("#ccbSaveBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.FILE_SAVE); });
311+
$("#ccbUndoBtn").on("click", function (e) {
312+
e.preventDefault();
313+
_ccbClickMetric("undo");
314+
_executeCmd(Commands.EDIT_UNDO);
315+
});
316+
$("#ccbRedoBtn").on("click", function (e) {
317+
e.preventDefault();
318+
_ccbClickMetric("redo");
319+
_executeCmd(Commands.EDIT_REDO);
320+
});
321+
$("#ccbSaveBtn").on("click", function (e) {
322+
e.preventDefault();
323+
_ccbClickMetric("save");
324+
_executeCmd(Commands.FILE_SAVE);
325+
});
309326
$("#ccbCollapseEditorBtn").on("click", function (e) {
310327
e.preventDefault();
328+
// editorCollapsed reflects the state *before* the toggle
329+
// executes; record which direction the user is going.
330+
_ccbClickMetric(editorCollapsed ? "designOff" : "designOn");
311331
CommandManager.execute(Commands.VIEW_TOGGLE_DESIGN_MODE);
312332
});
313333
$("#ccbSidebarToggleBtn").on("click", function (e) {
314334
e.preventDefault();
335+
_ccbClickMetric("sidebar");
315336
_executeCmd(Commands.VIEW_HIDE_SIDEBAR);
316337
});
317338
$("#ccbFileLabel").on("click", function (e) {
318339
e.preventDefault();
340+
_ccbClickMetric("file");
319341
_executeCmd(Commands.NAVIGATE_SHOW_IN_FILE_TREE);
320342
});
321343
}

src/view/SidebarTabs.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ define(function (require, exports, module) {
3939

4040
const AppInit = require("utils/AppInit"),
4141
EventDispatcher = require("utils/EventDispatcher"),
42+
Metrics = require("utils/Metrics"),
4243
PreferencesManager = require("preferences/PreferencesManager");
4344

4445
// --- Constants -----------------------------------------------------------
@@ -495,6 +496,14 @@ define(function (require, exports, module) {
495496
$navTabBar.on("click", ".sidebar-tab", function () {
496497
const tabId = $(this).attr("data-tab-id");
497498
if (tabId) {
499+
// Track only real user clicks here (programmatic
500+
// setActiveTab calls — e.g. the phoenix-tour AI peek —
501+
// shouldn't inflate the click metric). Map the built-in
502+
// Files tab id to a short label; pass the (already short)
503+
// id for the rest. Triple stays well within the metrics
504+
// server's per-event length budget.
505+
const label = (tabId === SIDEBAR_TAB_FILES) ? "files" : tabId;
506+
Metrics.countEvent(Metrics.EVENT_TYPE.UI, "navTab", label);
498507
setActiveTab(tabId);
499508
}
500509
});

0 commit comments

Comments
 (0)