diff --git a/src-mdviewer/src/bridge.js b/src-mdviewer/src/bridge.js index 3f87a0581f..684313c2fa 100644 --- a/src-mdviewer/src/bridge.js +++ b/src-mdviewer/src/bridge.js @@ -1430,3 +1430,16 @@ function sendToParent(eventName, payload) { ...payload }, "*"); } + +// ─── Metrics helpers ─────────────────────────────────────────────── +// Forwards metric events to the host (MarkdownSync.js) which routes them +// to Metrics.countEvent / valueEvent under EVENT_TYPE.MD. Only metadata +// (subcat, label, optional numeric value) crosses the boundary — never +// markdown content, file paths, or user keystrokes. +export function metricCount(subcat, label) { + sendToParent("mdviewrMetric", { kind: "count", subcat, label }); +} + +export function metricValue(subcat, label, value) { + sendToParent("mdviewrMetric", { kind: "value", subcat, label, value }); +} diff --git a/src-mdviewer/src/components/editor.js b/src-mdviewer/src/components/editor.js index 19009c7230..0bdbf284ed 100644 --- a/src-mdviewer/src/components/editor.js +++ b/src-mdviewer/src/components/editor.js @@ -12,6 +12,7 @@ import { initImagePopover, destroyImagePopover } from "./image-popover.js"; import { initLangPicker, destroyLangPicker, isLangPickerDropdownOpen } from "./lang-picker.js"; import { highlightCode, renderAfterHTML, normalizeCodeLanguages } from "./viewer.js"; import { initMermaidEditor, destroyMermaidEditor, insertMermaidBlock, attachOverlays } from "./mermaid-editor.js"; +import { metricCount } from "../bridge.js"; const devLog = import.meta.env.DEV ? console.log.bind(console, "[editor]") : () => {}; let turndown = null; @@ -713,6 +714,10 @@ function handleImagePaste(e, contentEl) { for (let i = 0; i < items.length; i++) { if (items[i].kind === "file" && ALLOWED_IMAGE_TYPES.includes(items[i].type)) { e.preventDefault(); + // Image paste in the live-preview iframe (paired with + // md/image/pasteCM for the CodeMirror editor path). No + // image data is recorded — just the count. + metricCount("image", "pasteLP"); const blob = items[i].getAsFile(); const fileName = blob.name || ("image." + blob.type.split("/")[1]); const uploadId = _insertUploadPlaceholder(contentEl); @@ -1005,6 +1010,17 @@ function showHandleMenu(anchor, type, ctx, contentEl, wrapper, clickX) { ]; } + // Wrap each action so picking any row/col menu item raises a single + // metric (md/table/rowEdit or colEdit). We don't record which + // specific edit — just that one happened — to keep cardinality low. + const editLabel = type === "row" ? "rowEdit" : "colEdit"; + items.forEach((it) => { + if (it.action) { + const orig = it.action; + it.action = () => { metricCount("table", editLabel); orig(); }; + } + }); + menu.innerHTML = ""; items.forEach((item) => { if (item.divider) { diff --git a/src-mdviewer/src/components/embedded-toolbar.js b/src-mdviewer/src/components/embedded-toolbar.js index 4589a98e75..8bce75653c 100644 --- a/src-mdviewer/src/components/embedded-toolbar.js +++ b/src-mdviewer/src/components/embedded-toolbar.js @@ -40,6 +40,16 @@ import { import { on, emit } from "../core/events.js"; import { getState, setState } from "../core/state.js"; import { t, tp } from "../core/i18n.js"; +import { metricCount } from "../bridge.js"; + +// Toolbar buttons whose clicks roll up under md/nav/formatClick (text- +// formatting and heading switches). Anything not in this set is treated +// as a generic md/nav/itemClick so we capture toolbar usage without +// fragmenting the metric across every button id. +const FORMAT_CLICK_IDS = new Set([ + "emb-bold", "emb-italic", "emb-strike", "emb-underline", "emb-code", + "emb-block-type" // heading dropdown +]); let toolbar = null; let resizeObserver = null; @@ -264,6 +274,14 @@ function wireFormatButtons() { if (el) { el.addEventListener("mousedown", (e) => { e.preventDefault(); + // Generic toolbar usage metric: format vs item bucket. + // Image insert specifically is also tracked under + // md/image/insert below for cross-feature roll-up. + metricCount("nav", + FORMAT_CLICK_IDS.has(binding.id) ? "formatClick" : "itemClick"); + if (binding.id === "emb-image-url" || binding.id === "emb-image-upload") { + metricCount("image", "insert"); + } emit("action:format", { command: binding.command, value: binding.value }); }); } @@ -274,6 +292,9 @@ function wireBlockTypeSelect() { const blockTypeSelect = document.getElementById("emb-block-type"); if (blockTypeSelect) { blockTypeSelect.addEventListener("change", (e) => { + // Heading dropdown rolls up under formatClick (heading + // selection is a formatting action, just delivered via select). + metricCount("nav", "formatClick"); emit("action:format", { command: "formatBlock", value: e.target.value }); e.target.blur(); }); @@ -347,6 +368,7 @@ function wirePrintButton() { const printBtn = document.getElementById("emb-print-btn"); if (printBtn) { printBtn.addEventListener("click", () => { + metricCount("print", "click"); window.print(); }); } diff --git a/src-mdviewer/src/components/slash-menu.js b/src-mdviewer/src/components/slash-menu.js index c18faba7e5..1db369b1cd 100644 --- a/src-mdviewer/src/components/slash-menu.js +++ b/src-mdviewer/src/components/slash-menu.js @@ -20,6 +20,7 @@ import { import { emit } from "../core/events.js"; import { getSelectionRect } from "./editor.js"; import { t } from "../core/i18n.js"; +import { metricCount } from "../bridge.js"; let menu = null; let contentEl = null; @@ -221,6 +222,9 @@ function show() { const rect = _savedSlashRect; const anchor = document.getElementById("slash-menu-anchor"); if (!anchor) return; + // Slash popup appearance metric — fires when the menu actually + // becomes visible, not on every keystroke that filters items. + metricCount("slash", "popup"); // Position anchor below the cursor line with gap const lineHeight = rect.bottom - rect.top; @@ -273,6 +277,9 @@ function selectItem(index) { if (index < 0 || index >= filteredItems.length) return; const item = filteredItems[index]; recordUsage(item.labelKey); + // Slash selection metric — does not record which item was picked + // (recordUsage already tracks that locally for prioritisation). + metricCount("slash", "select"); // Capture slashRange before hide() clears it const savedRange = slashRange; diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 05475298c8..6717b3c349 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -1340,6 +1340,67 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, _log("Session:", currentSessionId); } + // Per-turn token usage: each SDKAssistantMessage carries the + // wrapped Anthropic API message whose `.usage` reflects what + // that single turn consumed. Useful for diagnosing runaway + // loops; logged but not metric'd individually (the result + // message rolls up the session totals). + if (message.type === "assistant" && + message.message && message.message.usage) { + const u = message.message.usage; + _log("Turn usage:", + "in=" + (u.input_tokens || 0), + "out=" + (u.output_tokens || 0), + "cacheRead=" + (u.cache_read_input_tokens || 0), + "cacheCreate=" + (u.cache_creation_input_tokens || 0), + message.parent_tool_use_id ? "(subagent)" : ""); + } + + // Aggregate session usage on the terminal `result` message. + // The SDK emits exactly one of these per query (success or + // error_*) with totals across all turns and the per-model + // breakdown. + if (message.type === "result") { + const u = message.usage || {}; + const mu = message.modelUsage || {}; + _log("Result:", + "turns=" + (message.num_turns || 0), + "in=" + (u.input_tokens || 0), + "out=" + (u.output_tokens || 0), + "cacheRead=" + (u.cache_read_input_tokens || 0), + "cacheCreate=" + (u.cache_creation_input_tokens || 0), + "cost=$" + (message.total_cost_usd || 0).toFixed(4), + "ms=" + (message.duration_ms || 0), + "apiMs=" + (message.duration_api_ms || 0), + "subtype=" + message.subtype); + for (const modelName of Object.keys(mu)) { + const m = mu[modelName]; + _log("Model usage[" + modelName + "]:", + "in=" + (m.inputTokens || 0), + "out=" + (m.outputTokens || 0), + "cacheRead=" + (m.cacheReadInputTokens || 0), + "cacheCreate=" + (m.cacheCreationInputTokens || 0), + "websearch=" + (m.webSearchRequests || 0), + "cost=$" + (m.costUSD || 0).toFixed(4), + "ctxWindow=" + (m.contextWindow || 0)); + } + // Forward to the browser so AIChatPanel can raise metrics. + // Stuck on its own event so the existing aiComplete handler + // doesn't have to change shape. + nodeConnector.triggerPeer("aiUsage", { + requestId: requestId, + sessionId: currentSessionId, + subtype: message.subtype, + isError: !!message.is_error, + numTurns: message.num_turns || 0, + durationMs: message.duration_ms || 0, + durationApiMs: message.duration_api_ms || 0, + totalCostUSD: message.total_cost_usd || 0, + usage: u, + modelUsage: mu + }); + } + // Handle streaming events if (message.type === "stream_event") { const event = message.event; diff --git a/src/editor/EditorCommandHandlers.js b/src/editor/EditorCommandHandlers.js index 7a8aa3669d..4a726d303b 100644 --- a/src/editor/EditorCommandHandlers.js +++ b/src/editor/EditorCommandHandlers.js @@ -41,6 +41,7 @@ define(function (require, exports, module) { ChangeHelper = require("editor/EditorHelper/ChangeHelper"), LanguageManager = require("language/LanguageManager"), ImageUploadManager = require("features/ImageUploadManager"), + Metrics = require("utils/Metrics"), Dialogs = require("widgets/Dialogs"), AppInit = require("utils/AppInit"); @@ -1316,6 +1317,11 @@ define(function (require, exports, module) { event.preventDefault(); + // Metric: image paste in the CodeMirror markdown editor view + // (paired with md/image/pasteLP for the live-preview iframe path). + // No image data or filename is recorded — just the count. + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "image", "pasteCM"); + const blob = imageItem.getAsFile(); const fileName = blob.name || ("image." + blob.type.split("/")[1]); const provider = ImageUploadManager.getImageUploadProvider(); diff --git a/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js b/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js index 34c930b5f2..97902ade7d 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js @@ -29,6 +29,7 @@ define(function (require, exports, module) { CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), KeyBindingManager = require("command/KeyBindingManager"), + Metrics = require("utils/Metrics"), utils = require("./utils"); // Commands whose shortcuts, when forwarded from the md viewer iframe, @@ -42,6 +43,29 @@ define(function (require, exports, module) { Commands.CMD_FIND_IN_FILES ]; + // Set of file paths the user has edited in the markdown viewer this + // session. Used to fire md/doc/edited at most once per file so the + // metric tells us "users who edit at least one md doc" without + // inflating with every keystroke. + const _mdEditedFiles = new Set(); + + // Session-cumulative edit-batch count for distinguishing light vs + // heavy markdown editors. Each iframe content change (already + // 50ms-debounced upstream) increments this. When the count crosses + // a bucket threshold we fire a one-shot count event so the analytics + // funnel reads as: users-with-LTE5 ⊇ LTE25 ⊇ LTE100 ⊇ GT500/LTE500 + // ⊇ LTE1K ⊇ GT1K — a coarse-to-fine view of edit volume per session. + let _mdEditCount = 0; + const MD_EDIT_BUCKETS = [ + { threshold: 5, label: "LTE5" }, + { threshold: 25, label: "LTE25" }, + { threshold: 100, label: "LTE100" }, + { threshold: 500, label: "GT500" }, + { threshold: 500, label: "LTE500" }, + { threshold: 1000, label: "LTE1K" }, + { threshold: 1001, label: "GT1K" } + ]; + let _active = false; let _doc = null; let _$iframe = null; @@ -127,6 +151,12 @@ define(function (require, exports, module) { _handleRedo(); break; case "mdviewrEditModeChanged": + // Mode switch metric — reader/edit are the only two + // states the iframe reports. Fires on every transition + // (including init) so we see how often users go in and + // out of edit mode. + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "mode", + data.editMode ? "edit" : "reader"); // When switching to reader, send CM content so the iframe // can re-render with accurate data-source-line for cursor sync. if (!data.editMode && _doc) { @@ -161,6 +191,30 @@ define(function (require, exports, module) { if (_onThemeToggle) { _onThemeToggle(data.theme); } + // Theme switch metric — fires on every user toggle. The + // initial state (light vs dark) is reported separately + // when the viewer first activates a markdown doc. + if (data.theme === "dark" || data.theme === "light") { + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "theme", data.theme); + } + break; + case "mdviewrMetric": + // Generic metric forwarder for events the iframe wants + // to record. Schema: + // { kind: "count" | "value", subcat, label, [value] } + // We validate types before forwarding so a malformed + // payload from the iframe can't break the metrics + // pipeline. Category is always EVENT_TYPE.MD. + if (data && typeof data.subcat === "string" && data.subcat && + typeof data.label === "string" && data.label) { + if (data.kind === "value" && Number.isFinite(data.value)) { + Metrics.valueEvent(Metrics.EVENT_TYPE.MD, + data.subcat, data.label, data.value); + } else { + Metrics.countEvent(Metrics.EVENT_TYPE.MD, + data.subcat, data.label); + } + } break; case "mdviewrImageUploadRequest": _handleImageUploadFromIframe(data); @@ -390,6 +444,15 @@ define(function (require, exports, module) { _sendTheme(); _sendLocale(); _sendSkipRefocusShortcuts(); + // Record the effective theme the user is starting in + // (light vs dark). Mirror of mdviewrThemeToggle so the dark/ + // light counter reflects both initial state and toggles. + const initialTheme = _themeOverride + || ((ThemeManager.getCurrentTheme() && ThemeManager.getCurrentTheme().dark) + ? "dark" : "light"); + if (initialTheme === "dark" || initialTheme === "light") { + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "theme", initialTheme); + } if (_onIframeReadyCallback) { _onIframeReadyCallback(); } @@ -622,6 +685,27 @@ define(function (require, exports, module) { _applyDiffToEditor(markdown); + // First-edit metric per file per session — fires at most once + // per document so "users who edit any md file" is a clean count. + const editedPath = _doc && _doc.file && _doc.file.fullPath; + if (editedPath && !_mdEditedFiles.has(editedPath)) { + _mdEditedFiles.add(editedPath); + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "doc", "edited"); + } + + // Edit-volume bucket: fire once when the cumulative session + // edit count first crosses each threshold. Each bucket fires + // at most once per session, so the metric reads as a funnel. + // No early-out: multiple labels at the same threshold all + // fire (e.g. GT500 + LTE500 at 500 batches). + _mdEditCount++; + for (let i = 0; i < MD_EDIT_BUCKETS.length; i++) { + if (_mdEditCount === MD_EDIT_BUCKETS[i].threshold) { + Metrics.countEvent(Metrics.EVENT_TYPE.MD, + "edits", MD_EDIT_BUCKETS[i].label); + } + } + // Send back the actual CM text so the iframe can compute accurate // data-source-line attributes. The markdown from convertToMarkdown // may differ slightly from CM's content (e.g. table formatting), diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 933cb12a17..850c374b7d 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -755,6 +755,12 @@ define(function (require, exports, module) { */ function _handlePreviewBtnClick() { const currentMode = LiveDevelopment.getCurrentMode(); + // Metric: which direction the user is toggling so we can see + // how often the inline-edit affordance is actually used vs + // simply opened-then-closed. + Metrics.countEvent(Metrics.EVENT_TYPE.LP_EDIT, "modeBtn", + currentMode === LiveDevelopment.CONSTANTS.LIVE_EDIT_MODE + ? "toPrev" : "toEdit"); if (currentMode === LiveDevelopment.CONSTANTS.LIVE_EDIT_MODE) { LiveDevelopment.setMode(LiveDevelopment.CONSTANTS.LIVE_PREVIEW_MODE); return; @@ -1620,6 +1626,9 @@ define(function (require, exports, module) { text: buttonGetProText } ]; Metrics.countEvent(Metrics.EVENT_TYPE.LP_EDIT, "mdEditUpsell", "show"); + // Mirror under the MD category so it groups with the rest + // of the markdown-editor metrics on the dashboard. + Metrics.countEvent(Metrics.EVENT_TYPE.MD, "upsell", "shown"); Dialogs.showModalDialog(Dialogs.DIALOG_ID_INFO, Strings.AVAILABLE_IN_PRO_TITLE, Strings.MD_EDIT_UPSELL_MESSAGE, buttons) diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index 0d8e19571b..2caa47661d 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { const WorkspaceManager = require("view/WorkspaceManager"); const ProjectManager = require("project/ProjectManager"); const ExtensionUtils = require("utils/ExtensionUtils"); + const Metrics = require("utils/Metrics"); const NodeConnector = require("NodeConnector"); const Mustache = require("thirdparty/mustache/mustache"); const Dialogs = require("widgets/Dialogs"); @@ -167,6 +168,7 @@ define(function (require, exports, module) { active.focus(); } _showFocusHintToast(); + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "panel", "open"); } }); @@ -199,6 +201,11 @@ define(function (require, exports, module) { ShellProfiles.setDefaultShell(shell.name); _populateShellDropdown(); _updateNewTerminalButtonLabel(); + // Metric: which shell the user picked from the dropdown + // (default-shell switch). _createNewTerminalWithShell below + // will also raise its own "new" metric for the spawn. + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "pick", + _shellMetricLabel(shell.name)); _createNewTerminalWithShell(shell); }); $shellDropdown.append($item); @@ -290,11 +297,33 @@ define(function (require, exports, module) { * @param {Object} shell - Shell profile to use * @param {string} [cwdOverride] - Optional VFS path to use as cwd instead of project root */ + /** + * Map an OS shell name (e.g. "powershell.exe", "bash.exe") to a short + * family label so the metrics server's per-event length budget stays + * comfortable. Strips ".exe" and lower-cases; unknown shells fall + * through under "other" (with their lower-cased name shown only in + * the cap'd 8-char form). + */ + function _shellMetricLabel(shellName) { + if (!shellName) { return "unknown"; } + let n = String(shellName).toLowerCase(); + if (n.endsWith(".exe")) { n = n.slice(0, -4); } + // pwsh & powershell are functionally the same family for the metric. + if (n === "powershell") { n = "pwsh"; } + // Cap so an unexpected long shell name can't blow the label cell. + if (n.length > 8) { n = n.slice(0, 8); } + return n || "unknown"; + } + async function _createNewTerminalWithShell(shell, cwdOverride) { if (!shell) { console.error("Terminal: No shell available"); + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", "noShell"); return; } + // Metric: a new terminal session was created, keyed by shell family. + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "new", + _shellMetricLabel(shell.name)); // Get cwd: use override if provided, otherwise fall back to project root let cwd; @@ -320,6 +349,16 @@ define(function (require, exports, module) { // Add to list terminalInstances.push(instance); + // Tab-count bucket metric — raised only on tab creation so we + // can plot how many concurrent terminal tabs users keep open. + // Buckets: 1 → "one", 2..4 → "LTE4", 5..9 → "LTE9", 10+ → "GT10". + const count = terminalInstances.length; + const tabsBucket = count === 1 ? "one" + : count <= 4 ? "LTE4" + : count <= 9 ? "LTE9" + : "GT10"; + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "tabs", tabsBucket); + // Activate this terminal (also updates flyout) _activateTerminal(instance.id); @@ -394,6 +433,7 @@ define(function (require, exports, module) { instance.dispose(); terminalInstances.splice(idx, 1); delete processInfo[id]; + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "close", "user"); // If we closed the active terminal, activate another if (activeTerminalId === id) { @@ -563,6 +603,12 @@ define(function (require, exports, module) { function _onTerminalProcessExit(id, exitCode) { delete processInfo[id]; _updateFlyout(); + // Metric: terminal process exited on its own (e.g. user typed + // "exit"). Distinct from "user" close above, which records the + // X-button / panel-driven close path. Exit code is bucketed + // ok/err so cardinality stays bounded. + Metrics.countEvent(Metrics.EVENT_TYPE.TERMINAL, "exit", + exitCode === 0 ? "ok" : "err"); } /** diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js index 9d096e5722..5b05b447ea 100644 --- a/src/utils/Metrics.js +++ b/src/utils/Metrics.js @@ -129,6 +129,9 @@ define(function (require, exports, module) { GIT: "git", AUTH: "auth", PRO: "pro", + AI: "ai", + TERMINAL: "term", + MD: "md", GUIDE: "guide" }; diff --git a/src/view/CentralControlBar.js b/src/view/CentralControlBar.js index 69c9b0c160..23155f6ff4 100644 --- a/src/view/CentralControlBar.js +++ b/src/view/CentralControlBar.js @@ -25,6 +25,7 @@ define(function (require, exports, module) { const Commands = require("command/Commands"); const DocumentManager = require("document/DocumentManager"); const MainViewManager = require("view/MainViewManager"); + const Metrics = require("utils/Metrics"); const Strings = require("strings"); const WorkspaceManager = require("view/WorkspaceManager"); const SidebarView = require("project/SidebarView"); @@ -302,20 +303,41 @@ define(function (require, exports, module) { $btn.find("i").attr("class", isVisible ? "fa-solid fa-angles-left" : "fa-solid fa-angles-right"); } + function _ccbClickMetric(label) { + Metrics.countEvent(Metrics.EVENT_TYPE.UI, "ccb", label); + } + function _wireButtons() { - $("#ccbUndoBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.EDIT_UNDO); }); - $("#ccbRedoBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.EDIT_REDO); }); - $("#ccbSaveBtn").on("click", function (e) { e.preventDefault(); _executeCmd(Commands.FILE_SAVE); }); + $("#ccbUndoBtn").on("click", function (e) { + e.preventDefault(); + _ccbClickMetric("undo"); + _executeCmd(Commands.EDIT_UNDO); + }); + $("#ccbRedoBtn").on("click", function (e) { + e.preventDefault(); + _ccbClickMetric("redo"); + _executeCmd(Commands.EDIT_REDO); + }); + $("#ccbSaveBtn").on("click", function (e) { + e.preventDefault(); + _ccbClickMetric("save"); + _executeCmd(Commands.FILE_SAVE); + }); $("#ccbCollapseEditorBtn").on("click", function (e) { e.preventDefault(); + // editorCollapsed reflects the state *before* the toggle + // executes; record which direction the user is going. + _ccbClickMetric(editorCollapsed ? "designOff" : "designOn"); CommandManager.execute(Commands.VIEW_TOGGLE_DESIGN_MODE); }); $("#ccbSidebarToggleBtn").on("click", function (e) { e.preventDefault(); + _ccbClickMetric("sidebar"); _executeCmd(Commands.VIEW_HIDE_SIDEBAR); }); $("#ccbFileLabel").on("click", function (e) { e.preventDefault(); + _ccbClickMetric("file"); _executeCmd(Commands.NAVIGATE_SHOW_IN_FILE_TREE); }); } diff --git a/src/view/SidebarTabs.js b/src/view/SidebarTabs.js index a37da25a54..97736b9edc 100644 --- a/src/view/SidebarTabs.js +++ b/src/view/SidebarTabs.js @@ -39,6 +39,7 @@ define(function (require, exports, module) { const AppInit = require("utils/AppInit"), EventDispatcher = require("utils/EventDispatcher"), + Metrics = require("utils/Metrics"), PreferencesManager = require("preferences/PreferencesManager"); // --- Constants ----------------------------------------------------------- @@ -495,6 +496,14 @@ define(function (require, exports, module) { $navTabBar.on("click", ".sidebar-tab", function () { const tabId = $(this).attr("data-tab-id"); if (tabId) { + // Track only real user clicks here (programmatic + // setActiveTab calls — e.g. the phoenix-tour AI peek — + // shouldn't inflate the click metric). Map the built-in + // Files tab id to a short label; pass the (already short) + // id for the rest. Triple stays well within the metrics + // server's per-event length budget. + const label = (tabId === SIDEBAR_TAB_FILES) ? "files" : tabId; + Metrics.countEvent(Metrics.EVENT_TYPE.UI, "navTab", label); setActiveTab(tabId); } }); diff --git a/tracking-repos.json b/tracking-repos.json index c8366fb55f..5d26a3f6a2 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "568f1596cb1523acda5a8d882b46e3166a5eb2a2" + "commitID": "99559768f4fbbab86ed3589e9a51d3a915f51986" } }