diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md
index b5ae5e4f04..b975177529 100644
--- a/docs/API-Reference/command/Commands.md
+++ b/docs/API-Reference/command/Commands.md
@@ -542,6 +542,12 @@ Toggles problems panel visibility
## VIEW\_TERMINAL
Opens the terminal panel
+**Kind**: global variable
+
+
+## VIEW\_ENABLE\_AI
+Toggles whether AI features are enabled
+
**Kind**: global variable
diff --git a/src/editor/CodeHintManager.js b/src/editor/CodeHintManager.js
index 6135b31a23..a3b4644e07 100644
--- a/src/editor/CodeHintManager.js
+++ b/src/editor/CodeHintManager.js
@@ -252,7 +252,8 @@ define(function (require, exports, module) {
Strings = require("strings"),
KeyEvent = require("utils/KeyEvent"),
CodeHintListModule = require("editor/CodeHintList"),
- PreferencesManager = require("preferences/PreferencesManager");
+ PreferencesManager = require("preferences/PreferencesManager"),
+ Metrics = require("utils/Metrics");
const CodeHintList = CodeHintListModule.CodeHintList;
@@ -264,7 +265,9 @@ define(function (require, exports, module) {
deferredHints = null,
keyDownEditor = null,
codeHintsEnabled = true,
- codeHintOpened = false;
+ codeHintOpened = false,
+ sessionLanguageId = null, // language at cursor when the session began - metric label
+ sessionShownMetricSent = false; // one metric per session, not one per keystroke update
// API for extensions to show hints at the top
let hintsAtTopHandler = null;
@@ -284,8 +287,46 @@ define(function (require, exports, module) {
PreferencesManager.on("change", "showCodeHints", function () {
codeHintsEnabled = PreferencesManager.get("showCodeHints");
+ if (!codeHintsEnabled) {
+ // The opt-out signal: code hints in the non-default off state. Also fires on boot
+ // pref-load for users who keep it off - fine, it's a rough signal.
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "pref", "off");
+ }
});
+ /**
+ * Counts the hint list becoming visible with results, once per session. Also attributes
+ * the session to its language server when the provider is LSP-backed, so per-server usage
+ * (and acceptance rate vs the accept metric) is computable. Batched - hint sessions are
+ * per-keystroke-frequency events.
+ * @private
+ * @param {{hints: Array}} response the response about to be shown
+ */
+ function _recordHintsShownMetric(response) {
+ if (sessionShownMetricSent || !response || !response.hints || !response.hints.length) {
+ return;
+ }
+ sessionShownMetricSent = true;
+ Metrics.countEventBatched(Metrics.EVENT_TYPE.CODE_HINTS, "show", sessionLanguageId);
+ if (sessionProvider && sessionProvider.client && sessionProvider.client._metricLabel) {
+ Metrics.countEventBatched(Metrics.EVENT_TYPE.LSP, "hint",
+ "Show." + sessionProvider.client._metricLabel);
+ }
+ }
+
+ /**
+ * A provider threw synchronously (hasHints/getHints) - previously this propagated up
+ * through the typing handler and could wedge the session. Count it and report it once per
+ * language per app run.
+ * @private
+ */
+ function _recordProviderError(err, languageId) {
+ console.error("[CodeHints] provider threw in hint session (" + languageId + ")", err);
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "provErr", languageId);
+ window.logger.reportErrorOnce("hintProv." + languageId, err,
+ "[CodeHints] provider threw in hint session (" + languageId + ")");
+ }
+
/**
* Comparator to sort providers from high to low priority
* @private
@@ -406,6 +447,9 @@ define(function (require, exports, module) {
sessionProvider = null;
sessionEditor = null;
if (deferredHints) {
+ // Mark this as a manager-side cancellation so the fail handler attached in
+ // _updateHintList doesn't count it as a provider failure.
+ deferredHints._canceledByHintManager = true;
deferredHints.reject();
deferredHints = null;
}
@@ -445,6 +489,7 @@ define(function (require, exports, module) {
callMoveUpEvent = typeof callMoveUpEvent === "undefined" ? false : callMoveUpEvent;
if (deferredHints) {
+ deferredHints._canceledByHintManager = true; // superseded by a newer query, not a failure
deferredHints.reject();
deferredHints = null;
}
@@ -463,7 +508,14 @@ define(function (require, exports, module) {
// Get hints from regular provider if available
if (sessionProvider) {
- response = sessionProvider.getHints(lastChar);
+ try {
+ response = sessionProvider.getHints(lastChar);
+ } catch (e) {
+ // A throwing provider must not wedge the session - fall through to the
+ // no-response path below, which closes it cleanly.
+ _recordProviderError(e, sessionLanguageId);
+ response = null;
+ }
}
lastChar = null;
@@ -501,6 +553,7 @@ define(function (require, exports, module) {
}
}
+ _recordHintsShownMetric(response);
if (hintList.isOpen()) {
// the session is open
hintList.update(response);
@@ -509,6 +562,16 @@ define(function (require, exports, module) {
}
} else { // response is a deferred
deferredHints = response;
+ response.fail(function () {
+ // Provider-side failure (e.g. an LSP completion request errored) - the
+ // manager's own cancellations (superseded query / session end) are marked
+ // and excluded. Metrics only: per-server failures are counted at the LSP
+ // layer, this tracks how often users see hints silently not appear.
+ if (!response._canceledByHintManager) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "fail",
+ sessionLanguageId || "ukn");
+ }
+ });
response.done(function (hints) {
// Guard against timing issues where the session ends before the
// response gets a chance to execute the callback. If the session
@@ -527,6 +590,7 @@ define(function (require, exports, module) {
}
}
+ _recordHintsShownMetric(hints);
if (hintList.isOpen()) {
// the session is open
hintList.update(hints);
@@ -565,18 +629,30 @@ define(function (require, exports, module) {
var language = editor.getLanguageForSelection(),
enabledProviders = _getProvidersForLanguageId(language.getId());
+ sessionLanguageId = language.getId();
+ sessionShownMetricSent = false;
+
// Check if hints-at-top handler has hints first to avoid duplication
var hasTopHints = false;
if (hintsAtTopHandler && hintsAtTopHandler.hasHints) {
- hasTopHints = hintsAtTopHandler.hasHints(editor, lastChar);
+ try {
+ hasTopHints = hintsAtTopHandler.hasHints(editor, lastChar);
+ } catch (e) {
+ _recordProviderError(e, sessionLanguageId);
+ }
}
// Find a suitable provider only if hints-at-top handler doesn't have hints
if (!hasTopHints) {
enabledProviders.some(function (item, index) {
- if (item.provider.hasHints(editor, lastChar)) {
- sessionProvider = item.provider;
- return true;
+ try {
+ if (item.provider.hasHints(editor, lastChar)) {
+ sessionProvider = item.provider;
+ return true;
+ }
+ } catch (e) {
+ // Skip the throwing provider; lower-priority providers still get a shot.
+ _recordProviderError(e, sessionLanguageId);
}
});
}
@@ -610,25 +686,37 @@ define(function (require, exports, module) {
}
});
hintList.onSelect(function (hint) {
- // allow extensions to handle special hint selections
- var handled = false;
- if (hintsAtTopHandler && hintsAtTopHandler.insertHint) {
- handled = hintsAtTopHandler.insertHint(hint);
- }
+ var acceptedLanguageId = sessionLanguageId;
+ try {
+ // allow extensions to handle special hint selections
+ var handled = false;
+ if (hintsAtTopHandler && hintsAtTopHandler.insertHint) {
+ handled = hintsAtTopHandler.insertHint(hint);
+ }
- if (handled) {
- // If hints-at-top handler handled it, end the session
- _endSession();
- } else if (sessionProvider) {
- // Regular hint provider handling
- var restart = sessionProvider.insertHint(hint),
- previousEditor = sessionEditor;
- _endSession();
- if (restart) {
- _beginSession(previousEditor);
+ if (handled) {
+ // If hints-at-top handler handled it, end the session
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "accept", acceptedLanguageId);
+ _endSession();
+ } else if (sessionProvider) {
+ // Regular hint provider handling
+ var restart = sessionProvider.insertHint(hint),
+ previousEditor = sessionEditor;
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "accept", acceptedLanguageId);
+ _endSession();
+ if (restart) {
+ _beginSession(previousEditor);
+ }
+ } else {
+ // if none of the provider handled it, we just end the session
+ _endSession();
}
- } else {
- // if none of the provider handled it, we just end the session
+ } catch (e) {
+ // An insertHint throw used to skip _endSession and leave a stuck popup.
+ console.error("[CodeHints] insertHint failed (" + acceptedLanguageId + ")", e);
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "insErr", acceptedLanguageId);
+ window.logger.reportErrorOnce("hintInsert." + acceptedLanguageId, e,
+ "[CodeHints] insertHint failed (" + acceptedLanguageId + ")");
_endSession();
}
});
diff --git a/src/extensibility/ExtensionManagerDialog.js b/src/extensibility/ExtensionManagerDialog.js
index 6f182fe437..c74f5161ff 100644
--- a/src/extensibility/ExtensionManagerDialog.js
+++ b/src/extensibility/ExtensionManagerDialog.js
@@ -432,7 +432,10 @@ define(function (require, exports, module) {
CommandManager.register(Strings.CMD_EXTENSION_MANAGER, Commands.FILE_EXTENSION_MANAGER, _showDialog, { supportsDesignMode: true });
AppInit.appReady(function () {
- $("#toolbar-extension-manager").click(_showDialog);
+ $("#toolbar-extension-manager").click(function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.UI, "toolbarBtn", "extnMgr");
+ _showDialog();
+ });
});
// Unit tests
diff --git a/src/extensions/default/HealthData/HealthDataManager.js b/src/extensions/default/HealthData/HealthDataManager.js
index bd43e0ab6f..8d798c3ffa 100644
--- a/src/extensions/default/HealthData/HealthDataManager.js
+++ b/src/extensions/default/HealthData/HealthDataManager.js
@@ -120,7 +120,7 @@ define(function (require, exports, module) {
//
// To track this discrepancy, we emit a one-time metric just before disabling tracking,
// so we’re aware of this inconsistency and can address it if needed.
- Metrics.countEvent(Metrics.PLATFORM, "metricBoot", "disableErr");
+ Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "metricBoot", "disableErr");
}
Metrics.setDisabled(healthDataDisabled);
SendToAnalytics.sendPlatformMetrics();
diff --git a/src/extensions/default/TypeScriptSupport/unittests.js b/src/extensions/default/TypeScriptSupport/unittests.js
index 21b08a2aae..bc8aa1c349 100644
--- a/src/extensions/default/TypeScriptSupport/unittests.js
+++ b/src/extensions/default/TypeScriptSupport/unittests.js
@@ -18,7 +18,7 @@
*
*/
-/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, path, jsPromise */
+/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, path, jsPromise, spyOn */
define(function (require, exports, module) {
@@ -946,5 +946,141 @@ define(function (require, exports, module) {
expect(panelText().includes("CommonJS")).toBe(false);
}, 90000);
});
+
+ describe("usage metrics", function () {
+ // Metrics go through the audit map even in the test environment (senders are
+ // disabled, the audit log is not), so assertions read getLoggedDataForAudit().
+ let Metrics;
+
+ beforeAll(function () {
+ Metrics = testWindow.brackets.getModule("utils/Metrics");
+ });
+
+ beforeEach(async function () {
+ // Earlier suites leave dirty unsaved documents behind; a project switch with
+ // those open would block on the save prompt and time the spec out.
+ await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE_ALL, { _forceClose: true }),
+ "close all files");
+ });
+
+ it("should count server starts and time completion round trips per server", async function () {
+ const audit = Metrics.getLoggedDataForAudit();
+ // the warm-up already started the typescript server at least once
+ const startEntry = audit.get("lsp.srv.Start.typescript");
+ expect(startEntry && startEntry.sum >= 1).toBe(true);
+
+ await _openInProject("ts/", "type-error.ts");
+ const editor = EditorManager.getActiveEditor();
+ editor.document.setText("const arr = [1, 2, 3];\narr.");
+ editor.setCursorPos(1, 4); // after `arr.`
+
+ const TsMain = await new Promise(function (resolve, reject) {
+ testWindow.brackets.getModule("utils/ExtensionLoader")
+ .getRequireContextForExtension("TypeScriptSupport")(["main"], resolve, reject);
+ });
+ const client = TsMain._getClient();
+ expect(client).toBeTruthy();
+
+ // Drive the LSP hint provider directly (the popup UI wants OS focus). A
+ // successful completion round trip must record batched latency under
+ // lsp.time.typescriptComp - visible in the audit only after a flush.
+ let gotHints = false;
+ await awaitsFor(function () {
+ if (gotHints) {
+ return true;
+ }
+ client.codeHints.getHints(null).done(function (result) {
+ if (result && result.hints && result.hints.length) {
+ gotHints = true;
+ }
+ });
+ return false;
+ }, "completions at arr. from the typescript server", 30000, 500);
+
+ await Metrics.flushMetrics();
+ const timeEntry = audit.get("lsp.time.Comp.typescript");
+ expect(timeEntry && timeEntry.count >= 1).toBe(true);
+ expect(timeEntry.eventType).toBe("val");
+
+ await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
+ "close type-error.ts");
+ }, 60000);
+
+ it("should count an LSP hint acceptance (and auto-imports) on insertHint", async function () {
+ await _openInProject("ts/", "type-error.ts");
+ const editor = EditorManager.getActiveEditor();
+ editor.document.setText("const arr = [1, 2, 3];\narr.pu");
+ editor.setCursorPos(1, 6); // after `arr.pu`
+
+ const TsMain = await new Promise(function (resolve, reject) {
+ testWindow.brackets.getModule("utils/ExtensionLoader")
+ .getRequireContextForExtension("TypeScriptSupport")(["main"], resolve, reject);
+ });
+ const client = TsMain._getClient();
+ let $pushHint = null;
+ await awaitsFor(function () {
+ if ($pushHint) {
+ return true;
+ }
+ client._completionCache = null; // always request fresh
+ client.codeHints.getHints(null).done(function (result) {
+ const hints = (result && result.hints) || [];
+ $pushHint = hints.find(function ($hint) {
+ const token = $hint.data("token");
+ return token && token.label === "push";
+ }) || null;
+ });
+ return false;
+ }, "a push completion at arr.pu", 30000, 500);
+
+ const audit = Metrics.getLoggedDataForAudit();
+ const before = audit.get("lsp.hint.Acc.typescript");
+ const beforeSum = (before && before.sum) || 0;
+ client.codeHints.insertHint($pushHint); // what selecting the hint with Enter does
+ const after = audit.get("lsp.hint.Acc.typescript");
+ expect(after && after.sum).toBeGreaterThan(beforeSum);
+ expect(editor.document.getLine(1)).toContain("push");
+
+ await awaitsForDone(CommandManager.execute(Commands.FILE_CLOSE, { _forceClose: true }),
+ "close type-error.ts");
+ }, 60000);
+
+ it("should report an error only once per key via logger.reportErrorOnce", function () {
+ const logger = testWindow.logger;
+ // In the test environment Bugsnag is disabled, so reportError degrades to
+ // console.error - spy on that to observe the dedup.
+ const spy = spyOn(testWindow.console, "error");
+ const key = "unitTest.reportOnce." + Date.now();
+ logger.reportErrorOnce(key, new Error("boom"), "first");
+ logger.reportErrorOnce(key, new Error("boom"), "second");
+ expect(spy.calls.count()).toBe(1);
+ logger.reportErrorOnce(key + ".other", new Error("boom"), "third");
+ expect(spy.calls.count()).toBe(2);
+ });
+
+ it("should scrub user paths from report messages but keep internal/system paths", function () {
+ const scrub = testWindow.logger._scrubPathsInText;
+ // home prefix cut + user project path redacted
+ expect(scrub("failed on /home/alice/projects/clientX/app.js"))
+ .toBe("failed on ");
+ expect(scrub("failed on C:\\Users\\alice\\Documents\\secret\\app.js"))
+ .toBe("failed on ");
+ // app-managed dirs under home stay after the username is cut
+ expect(scrub("server at /home/alice/.local/share/phcode/lsp/intelephense.js"))
+ .toBe("server at ~/.local/share/phcode/lsp/intelephense.js");
+ // machine-generic system paths and app origins stay verbatim
+ expect(scrub("spawn /usr/bin/node failed")).toBe("spawn /usr/bin/node failed");
+ expect(scrub("at phtauri://localhost/assets/main.js:1:1"))
+ .toBe("at phtauri://localhost/assets/main.js:1:1");
+ // non-path text untouched
+ expect(scrub("code=1 signal=none")).toBe("code=1 signal=none");
+ // must never throw, whatever it is fed - fails closed to a redacted placeholder
+ expect(scrub(null)).toBe("null");
+ expect(scrub(undefined)).toBe("undefined");
+ expect(scrub(42)).toBe("42");
+ expect(scrub({ toString: function () { throw new Error("boom"); } }))
+ .toBe("");
+ });
+ });
});
});
diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js
index 29e1649733..a9e10ae4e1 100644
--- a/src/extensionsIntegrated/Terminal/main.js
+++ b/src/extensionsIntegrated/Terminal/main.js
@@ -889,6 +889,7 @@ define(function (require, exports, module) {
.insertBefore("#app-drawer-button");
$btn.on("click", function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.UI, "toolbarBtn", "terminal");
if (WorkspaceManager.isInDesignMode()) {
CommandManager.execute(Commands.VIEW_TOGGLE_DESIGN_MODE);
CommandManager.execute(CMD_VIEW_TERMINAL);
diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js
index 872f0c28a7..bdc5cd0300 100644
--- a/src/features/FindReferencesManager.js
+++ b/src/features/FindReferencesManager.js
@@ -33,7 +33,8 @@ define(function (require, exports, module) {
ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler,
SearchResultsView = require("search/SearchResultsView").SearchResultsView,
SearchModel = require("search/SearchModel").SearchModel,
- Strings = require("strings");
+ Strings = require("strings"),
+ Metrics = require("utils/Metrics");
var _providerRegistrationHandler = new ProviderRegistrationHandler(),
removeFindReferencesProvider = _providerRegistrationHandler.removeProvider.bind(_providerRegistrationHandler);
@@ -101,10 +102,13 @@ define(function (require, exports, module) {
// If one of them will provide a widget, show it inline once ready
if (referencesPromise) {
referencesPromise.done(function () {
+ // Explicit user action - Ok vs None gives the per-language hit rate.
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "ref", "Ok." + language.getId());
if(_resultsView) {
_resultsView.open();
}
}).fail(function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "ref", "None." + language.getId());
if(_resultsView) {
_resultsView.close();
}
@@ -115,6 +119,7 @@ define(function (require, exports, module) {
if(_resultsView) {
_resultsView.close();
}
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "ref", "None." + language.getId());
editor.displayErrorMessageAtCursor(errorMsg);
result.reject();
}
diff --git a/src/features/JumpToDefManager.js b/src/features/JumpToDefManager.js
index bfa8307b75..6f476fafad 100644
--- a/src/features/JumpToDefManager.js
+++ b/src/features/JumpToDefManager.js
@@ -30,6 +30,7 @@ define(function (require, exports, module) {
CommandManager = require("command/CommandManager"),
EditorManager = require("editor/EditorManager"),
Editor = require("editor/Editor").Editor,
+ Metrics = require("utils/Metrics"),
ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler;
const JUMP_TO_DEF_MARKER = "jumpMarker";
@@ -70,21 +71,28 @@ define(function (require, exports, module) {
return result.promise();
}
+ // Explicit user action (Ctrl-J / menu) - Ok vs Fail gives the per-language success rate.
+ const languageId = editor.getLanguageForSelection().getId();
+
let jumpToDefProvider = _getJumpToDefProvider(editor);
if (!jumpToDefProvider) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "jump", "Fail." + languageId);
result.reject();
return result.promise();
}
request = jumpToDefProvider.doJumpToDef(editor);
if (!request) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "jump", "Fail." + languageId);
result.reject();
return result.promise();
}
request.done(function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "jump", "Ok." + languageId);
result.resolve();
}).fail(function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "jump", "Fail." + languageId);
result.reject();
});
diff --git a/src/features/ParameterHintsManager.js b/src/features/ParameterHintsManager.js
index f1e28d1dd0..ce1d903b97 100644
--- a/src/features/ParameterHintsManager.js
+++ b/src/features/ParameterHintsManager.js
@@ -35,6 +35,7 @@ define(function (require, exports, module) {
Strings = require("strings"),
PreferencesManager = require("preferences/PreferencesManager"),
WorkspaceManager = require("view/WorkspaceManager"),
+ Metrics = require("utils/Metrics"),
ProviderRegistrationHandler = require("features/PriorityBasedRegistration").RegistrationHandler;
@@ -99,6 +100,9 @@ define(function (require, exports, module) {
PreferencesManager.on("change", "showParameterHints", function () {
paramHintsEnabled = PreferencesManager.get("showParameterHints");
if (!paramHintsEnabled) {
+ // The opt-out signal: parameter hints in the non-default off state. Also fires on
+ // boot pref-load for users who keep it off - fine, it's a rough signal.
+ Metrics.countEvent(Metrics.EVENT_TYPE.CODE_HINTS, "pref", "sigOff");
dismissHint();
}
});
@@ -384,6 +388,12 @@ define(function (require, exports, module) {
return;
}
+ // Count the popup APPEARING, not every re-render while the caret moves within the
+ // call. Batched - typing through calls triggers this constantly.
+ if (!hintState.visible) {
+ Metrics.countEventBatched(Metrics.EVENT_TYPE.CODE_HINTS, "sig", language.getId());
+ }
+
_formatHint(editor, parameterHint);
$hintContainer.show(); // no-op when already visible -> content updates in place, no blink
@@ -507,6 +517,9 @@ define(function (require, exports, module) {
*/
function handleShowParameterHint() {
let editor = EditorManager.getActiveEditor();
+ // No metric here: CodeHintManager's explicit Ctrl-Space session also executes this
+ // command internally, so an "explicit param hint command" count would mostly measure
+ // code-hint invocations. The batched popup-shown metric is the usage signal.
// Pop up function hint
popUpHint(editor, true, false);
}
diff --git a/src/languageTools/DefaultProviders.js b/src/languageTools/DefaultProviders.js
index 18e0601b1e..2049d92523 100644
--- a/src/languageTools/DefaultProviders.js
+++ b/src/languageTools/DefaultProviders.js
@@ -40,6 +40,7 @@ define(function (require, exports, module) {
TabstopManager = require("editor/TabstopManager"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
+ Metrics = require("utils/Metrics"),
marked = require("thirdparty/marked.min"),
matcher = new StringMatch.StringMatcher({
preferPrefixMatches: true
@@ -541,6 +542,8 @@ define(function (require, exports, module) {
"selectInitial": true
});
}).fail(function () {
+ // requestHints rejects only on a real request failure - empty results resolve.
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Fail." + self.client._metricLabel);
self._importLabel = null;
$deferredHints.reject();
});
@@ -693,6 +696,12 @@ define(function (require, exports, module) {
{ line: te.range.end.line, ch: te.range.end.character });
});
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Acc." + this.client._metricLabel);
+ if (token.additionalTextEdits && token.additionalTextEdits.length) {
+ // An auto-import was applied with the completion - a high-value signal on its own.
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Import." + this.client._metricLabel);
+ }
+
// Return false to indicate that another hinting session is not needed
return false;
};
@@ -725,6 +734,7 @@ define(function (require, exports, module) {
var editor = EditorManager.getActiveEditor(),
pos = editor.getCursorPos(),
docPath = editor.document.file._path,
+ metricLabel = this.client._metricLabel,
$deferredHints = $.Deferred();
// A language server may VETO signature help at specific cursor positions by supplying a
@@ -781,7 +791,12 @@ define(function (require, exports, module) {
} else {
$deferredHints.reject();
}
- }).fail(function () {
+ }).fail(function (err) {
+ // requestParameterHints rejects bare for "no signatures here" (normal) and with an
+ // error for a real request failure - only the latter is a health signal.
+ if (err) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "sig", "Fail." + metricLabel);
+ }
$deferredHints.reject();
});
@@ -832,6 +847,7 @@ define(function (require, exports, module) {
const pos = editor.getCursorPos(),
docPath = editor.document.file._path,
docPathUri = PathConverters.pathToUri(docPath),
+ metricLabel = this.client._metricLabel,
$deferredHints = $.Deferred();
this.client.gotoDefinition({
@@ -871,7 +887,11 @@ define(function (require, exports, module) {
// the NAVIGATE_JUMPTO_DEFINITION command promise pending forever.
$deferredHints.reject();
}
- }).fail(function () {
+ }).fail(function (err) {
+ // gotoDefinition rejects bare for "no definition here" - only real failures count.
+ if (err) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "def", "Fail." + metricLabel);
+ }
$deferredHints.reject();
});
@@ -1178,6 +1198,10 @@ define(function (require, exports, module) {
}
} catch (err) {
console.warn("[LSP] quickfix fetch failed:", err && (err.message || err));
+ if (self._quickFixClient) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "fix",
+ "Fail." + self._quickFixClient._metricLabel);
+ }
}
};
@@ -1282,6 +1306,7 @@ define(function (require, exports, module) {
result = $.Deferred();
if (this.client) {
+ const metricLabel = this.client._metricLabel;
this.client.findReferences({
filePath: docPath,
cursorPos: pos
@@ -1294,7 +1319,10 @@ define(function (require, exports, module) {
} else {
result.reject();
}
- }).fail(function(){
+ }).fail(function(err){
+ if (err) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "ref", "Fail." + metricLabel);
+ }
result.reject();
});
return result.promise();
diff --git a/src/languageTools/HoverProvider.js b/src/languageTools/HoverProvider.js
index e7383abc52..77b61195e0 100644
--- a/src/languageTools/HoverProvider.js
+++ b/src/languageTools/HoverProvider.js
@@ -34,7 +34,8 @@ define(function (require, exports, module) {
Commands = require("command/Commands"),
KeyBindingManager = require("command/KeyBindingManager"),
QuickViewManager = require("features/QuickViewManager"),
- Strings = require("strings");
+ Strings = require("strings"),
+ Metrics = require("utils/Metrics");
/**
* Convert LSP hover `contents` (MarkupContent | MarkedString | string | array of those)
@@ -193,13 +194,22 @@ define(function (require, exports, module) {
if ($actions) {
$content.append($actions);
}
+ // Batched: fires on every dwell that produces a hover popup.
+ Metrics.countEventBatched(Metrics.EVENT_TYPE.LSP, "hover",
+ "Ok." + self.client._metricLabel);
resolve({
start: start,
end: end,
content: $content
});
})
- .fail(function () {
+ .fail(function (err) {
+ // requestHover rejects bare when there's simply nothing to show - only a
+ // real request failure is a health signal.
+ if (err) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hover",
+ "Fail." + self.client._metricLabel);
+ }
reject();
});
});
diff --git a/src/languageTools/LSPClient.js b/src/languageTools/LSPClient.js
index 81d07b8ee9..f9646cee99 100644
--- a/src/languageTools/LSPClient.js
+++ b/src/languageTools/LSPClient.js
@@ -68,7 +68,8 @@ define(function (require, exports, module) {
EventDispatcher = require("utils/EventDispatcher"),
PreferencesManager = require("preferences/PreferencesManager"),
Strings = require("strings"),
- StringUtils = require("utils/StringUtils");
+ StringUtils = require("utils/StringUtils"),
+ Metrics = require("utils/Metrics");
EventDispatcher.makeEventDispatcher(exports);
@@ -251,9 +252,16 @@ define(function (require, exports, module) {
console.error("[LSP] server '" + data.serverId + "' exited unexpectedly (code=" + data.code +
(data.signal ? ", signal=" + data.signal : "") + ")." +
(data.stderr ? "\n--- server stderr ---\n" + data.stderr : ""));
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "Crash." + client._metricLabel);
+ window.logger.leaveTrail("[LSP] " + client.serverId + " crashed, code=" + data.code);
client._crashCount = (client._crashCount || 0) + 1;
if (client._crashCount > MAX_AUTO_RESTARTS) {
console.error("[LSP]", client.serverId, "exited repeatedly; not restarting");
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "Gaveup." + client._metricLabel);
+ window.logger.reportErrorOnce("lspGaveup." + client.serverId,
+ new Error("code=" + data.code + " signal=" + (data.signal || "none") +
+ _stderrSnippet(data.stderr)),
+ "[LSP] server crashed repeatedly, gave up: " + client.serverId);
return;
}
setTimeout(function () {
@@ -267,13 +275,32 @@ define(function (require, exports, module) {
DocumentSync.openSupportedDocuments(client);
}).catch(function (err) {
console.error("[LSP] auto-restart failed", client.serverId, err && (err.message || err));
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "RstErr." + client._metricLabel);
});
}, 1000 * client._crashCount);
}
+ /**
+ * First non-empty stderr line, truncated, for the crash give-up report. Any user path in
+ * it is redacted by the loggerSetup message scrubber before the report leaves the app.
+ * @param {string} stderr
+ * @return {string} " stderr: " or ""
+ */
+ function _stderrSnippet(stderr) {
+ const lines = String(stderr || "").split("\n");
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (line) {
+ return " stderr: " + line.substring(0, 200);
+ }
+ }
+ return "";
+ }
+
function _onServerError(_event, data) {
if (data) {
console.error("[LSP] server error", data.serverId, data.error);
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "Err." + _srvLabel(data.serverId));
}
}
@@ -281,11 +308,37 @@ define(function (require, exports, module) {
// LanguageClient - one per server, exposes the provider-facing method surface
// ------------------------------------------------------------------------------------------
+ /**
+ * ServerId as a metric name part. Metric names must be short js-var-safe strings (the GA
+ * event name has a ~30 char budget), and plugin serverIds are unbounded - so sanitize and
+ * truncate once here; every metric in the LSP stack labels with this.
+ * @param {string} serverId
+ * @return {string}
+ */
+ function _srvLabel(serverId) {
+ const cleaned = String(serverId || "").replace(/[^a-zA-Z0-9]/g, "");
+ return cleaned.substring(0, 10) || "ukn";
+ }
+
+ // LSP request method -> short metric suffix for per-method latency. Anything unlisted is
+ // lumped into "Oth" so plugin/custom methods can't explode metric cardinality.
+ const METRIC_METHOD_GROUPS = {
+ "textDocument/completion": "Comp",
+ "textDocument/signatureHelp": "Sig",
+ "textDocument/hover": "Hover",
+ "textDocument/definition": "Def",
+ "textDocument/references": "Ref",
+ "textDocument/codeAction": "Fix",
+ "completionItem/resolve": "Res",
+ "textDocument/documentHighlight": "Hlt"
+ };
+
function LanguageClient(serverId, languages, config) {
this.serverId = serverId;
this.languages = languages;
this.config = config;
this.capabilities = null;
+ this._metricLabel = _srvLabel(serverId);
}
LanguageClient.prototype.getServerCapabilities = function () {
@@ -320,8 +373,16 @@ define(function (require, exports, module) {
LanguageClient.prototype._request = function (method, params) {
const serverId = this.serverId;
+ const metricLabel = this._metricLabel;
+ const startTime = Date.now();
return getConnector().then(function (conn) {
return conn.execPeer("sendRequest", { serverId: serverId, method: method, params: params });
+ }).then(function (result) {
+ // Successful requests only - failures include the 120s node-side timeout, which
+ // would wreck the averages. Failures are counted per feature instead.
+ Metrics.valueEventBatched(Metrics.EVENT_TYPE.LSP, "time",
+ (METRIC_METHOD_GROUPS[method] || "Oth") + "." + metricLabel, Date.now() - startTime);
+ return result;
});
};
@@ -726,6 +787,7 @@ define(function (require, exports, module) {
}
async function _startAndInit(client) {
+ const startTime = Date.now();
const config = client.config;
const conn = await getConnector();
const rootVfsPath = (config.rootUriProvider && config.rootUriProvider()) || _projectRootPath();
@@ -768,6 +830,9 @@ define(function (require, exports, module) {
method: "initialized",
params: {}
});
+ // Spawn + initialize handshake time - rare enough to send unbatched.
+ Metrics.valueEvent(Metrics.EVENT_TYPE.LSP, "time", "Init." + client._metricLabel,
+ Date.now() - startTime);
}
function _registerProviders(client) {
@@ -816,6 +881,7 @@ define(function (require, exports, module) {
* every path that brings a server up: initial registration, restart, and crash auto-restart.
*/
function _announceServerStarted(client) {
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "Start." + client._metricLabel);
exports.trigger(EVENT_LANGUAGE_SERVER_STARTED, {
serverId: client.serverId,
languages: client.languages
@@ -852,12 +918,25 @@ define(function (require, exports, module) {
return;
}
_prefWatchedServers.add(serverId);
+ // Change events also fire without the value flipping (e.g. pref scopes reloading on a
+ // project switch). Only a real flip may act - reacting to a same-value event would
+ // needlessly restart a running server (and miscount the opt-out metric).
+ let lastDisabled = _isDisabledByPref(serverId);
PreferencesManager.on("change", _codeIntelPrefKey(serverId), function () {
+ const disabledNow = _isDisabledByPref(serverId);
+ if (disabledNow === lastDisabled) {
+ return;
+ }
+ lastDisabled = disabledNow;
const client = clients.get(serverId);
if (!client) {
return;
}
- if (_isDisabledByPref(serverId)) {
+ if (disabledNow) {
+ // The opt-out signal: users actively turning code intelligence off. Only the
+ // non-default (off) state is logged - flips back to the default would just be
+ // noise.
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "PrefOff." + client._metricLabel);
if (client.capabilities) {
stopServerProcess(client).then(function () {
CodeInspection.requestRun();
@@ -922,6 +1001,9 @@ define(function (require, exports, module) {
}
_watchServerPref(config.serverId);
if (_isDisabledByPref(config.serverId)) {
+ // Users who keep code intelligence durably off. Registration retries can bump this
+ // more than once per run - fine, it's a rough opt-out signal, not an exact count.
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "OffBoot." + _srvLabel(config.serverId));
return null;
}
const client = new LanguageClient(config.serverId, config.languages, config);
@@ -939,6 +1021,12 @@ define(function (require, exports, module) {
return client;
} catch (err) {
console.error("[LSP] failed to start server", config.serverId, err && (err.message || err));
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "StartErr." + client._metricLabel);
+ // err.message distinguishes the classes (spawn error / spawn timeout / init
+ // failure - built node-side). Key shared with the restart-failed report so a
+ // broken server reports exactly once per app run whichever path hits first.
+ window.logger.reportErrorOnce("lspStart." + config.serverId, err,
+ "[LSP] start failed: " + config.serverId);
clients.delete(config.serverId);
return null;
}
@@ -1043,6 +1131,9 @@ define(function (require, exports, module) {
FindReferencesManager.setMenuItemStateForLanguage();
} catch (err) {
console.error("[LSP] failed to restart server", serverId, err && (err.message || err));
+ Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "srv", "RstErr." + client._metricLabel);
+ window.logger.reportErrorOnce("lspStart." + serverId, err,
+ "[LSP] restart failed: " + serverId);
}
}
diff --git a/src/loggerSetup.js b/src/loggerSetup.js
index 7f39c02bbd..b768e48f2b 100644
--- a/src/loggerSetup.js
+++ b/src/loggerSetup.js
@@ -46,6 +46,7 @@
MAX_ERR_SENT_FIRST_MINUTE = 10,
MAX_ERR_ALLOWED_IN_MINUTE = 2;
let firstMinuteElapsed = false, errorsSentThisMinute = 0;
+ const reportedOnceKeys = new Set();
class CustomBugSnagError extends Error {
constructor(message, err){
@@ -73,6 +74,21 @@
console.error(message, error, error.nodeStack);
}
},
+ /**
+ * Same as reportError, but reports at most once per `key` per app run. Use for
+ * failures that can repeat (a broken language server fails on every retry) so a
+ * single user cannot exhaust the app-wide error reporting budget with one issue.
+ * @param {string} key dedup key, e.g. "lspStart.typescript"
+ * @param {Error} error
+ * @param {string} [message] optional message
+ */
+ reportErrorOnce: function (key, error, message) {
+ if(reportedOnceKeys.has(key)){
+ return;
+ }
+ reportedOnceKeys.add(key);
+ logger.reportError(error, message);
+ },
/**
* By default all uncaught exceptions and promise rejections are sent to logger utility. But in some cases
* you may want to log error without having an error object with you.
@@ -214,9 +230,64 @@
return false;
}
+ // Paths that are safe to keep in reported error messages: they point at the app's own
+ // code, origins or machine-generic system locations - never at user content - and are
+ // valuable for diagnosis (e.g. language server stderr referencing its own install dir).
+ function _isSafePathForLogging(pathText) {
+ const lower = pathText.toLowerCase();
+ const safeStarts = ["phtauri://", "https://phtauri.localhost", "http://localhost",
+ "https://localhost", "https://phcode.dev", "https://create.phcode.dev",
+ "https://dev.phcode.dev", "https://staging.phcode.dev",
+ "/usr/", "/lib/", "/lib64/", "/opt/", "/snap/", "/bin/", "/sbin/", "/etc/",
+ "/system/", "/applications/", "c:\\program files", "c:\\windows"];
+ for(let prefix of safeStarts){
+ if(lower.startsWith(prefix)){
+ return true;
+ }
+ }
+ // App-managed dirs (installed language servers, caches) live under a phoenix/phcode
+ // named folder; after home-dir normalization they carry no user identity, so keep them.
+ if(lower.startsWith("~") && (lower.includes("phcode") || lower.includes("phoenix"))){
+ return true;
+ }
+ return false;
+ }
+
+ const HOME_DIR_PREFIX_REGEX = /(?:\/(?:home|Users)\/[^/\s'"`]+|[A-Za-z]:[\\/]Users[\\/][^\\/\s'"`]+)/g;
+ // URLs; ~-anchored paths (1+ segments - a bare ~/file.txt is still a user file); absolute
+ // paths need 2+ segments so prose like "and/or" is not treated as a path.
+ const PATH_LIKE_REGEX = /(?:[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s'"`)\]]+|~(?:[\\/][^\\/\s'"`)\]]+)+|(?:[\\/][^\\/\s'"`)\]]+){2,})/g;
+
+ // Redacts user paths from an error message while keeping internal app/system paths.
+ // The home-dir prefix is cut first (drops the username), then any remaining path-like
+ // substring not under a known-internal prefix is replaced with - a trimmed user
+ // path still leaks project/folder names, and the privacy bar here matches
+ // _shouldDiscardError which drops user-fs stacks entirely.
+ // Must NEVER throw: it runs inside onError, whose catch would swallow the throw and let
+ // the report leave UNSCRUBBED. On any internal failure it fails closed (fully redacted
+ // placeholder) rather than returning the original text.
+ function _scrubPathsInText(text) {
+ try {
+ const homeTrimmed = String(text).replace(HOME_DIR_PREFIX_REGEX, "~");
+ return homeTrimmed.replace(PATH_LIKE_REGEX, function (match) {
+ return _isSafePathForLogging(match) ? match : "";
+ });
+ } catch (e) {
+ return "";
+ }
+ }
+ logger._scrubPathsInText = _scrubPathsInText; // exposed for unit tests
+
function onError(event) {
// for more info https://docs.bugsnag.com/platforms/javascript/customizing-error-reports
try{
+ if(Array.isArray(event.errors)){
+ for(let error of event.errors){
+ if(error && error.errorMessage){
+ error.errorMessage = _scrubPathsInText(error.errorMessage);
+ }
+ }
+ }
let reportedStatus = "Reported";
let shouldReport = true;
if(logger.loggingOptions.healthDataDisabled
diff --git a/src/utils/Metrics.js b/src/utils/Metrics.js
index 5b05b447ea..ae47fd80f7 100644
--- a/src/utils/Metrics.js
+++ b/src/utils/Metrics.js
@@ -114,7 +114,7 @@ define(function (require, exports, module) {
LIVE_PREVIEW: "live-preview",
LP_EDIT: "lp-edit", // live preview edit related
KEYBOARD: "keyboard",
- CODE_HINTS: "code-hints",
+ CODE_HINTS: "hints",
EDITOR: "editor",
QUICK_VIEW: "quickView",
SEARCH: "search",
@@ -132,7 +132,8 @@ define(function (require, exports, module) {
AI: "ai",
TERMINAL: "term",
MD: "md",
- GUIDE: "guide"
+ GUIDE: "guide",
+ LSP: "lsp"
};
/**
@@ -449,6 +450,73 @@ define(function (require, exports, module) {
_valueEvent(eventType, eventCategory, eventSubCategory, value);
}
+ // High-frequency events (per-keystroke hint popups, per-request latencies) would overwhelm
+ // the GA free-tier event budget if sent per occurrence, so they are accumulated here and
+ // flushed as aggregates every BATCH_FLUSH_MS through the normal countEvent/valueEvent
+ // pipeline (which keeps power-user prefixing, the audit map and both sinks working).
+ const BATCH_FLUSH_MS = 120 * 1000;
+ const BATCH_KEY_SEPARATOR = "|";
+ const _batchedCounts = new Map(); // "type|category|label" -> count
+ const _batchedValues = new Map(); // "type|category|label" -> {sum, count}
+
+ /**
+ * Same as countEvent, but aggregated client side: occurrences are summed and emitted as a
+ * single count every 2 minutes (and on flushMetrics before app quit). Use for events that
+ * can fire many times a minute; low-frequency user actions should use countEvent directly
+ * so they are not delayed.
+ *
+ * @param {EVENT_TYPE|string} eventType The kind of Event Type that needs to be logged- should be
+ * a js var compatible string.
+ * @param {string} eventCategory The kind of Event Category that
+ * needs to be logged- should be a js var compatible string
+ * @param {string} eventSubCategory The kind of Event Sub Category that
+ * needs to be logged- should be a js var compatible string
+ * @type {function}
+ */
+ function countEventBatched(eventType, eventCategory, eventSubCategory) {
+ const key = eventType + BATCH_KEY_SEPARATOR + eventCategory + BATCH_KEY_SEPARATOR + eventSubCategory;
+ _batchedCounts.set(key, (_batchedCounts.get(key) || 0) + 1);
+ }
+
+ /**
+ * Same as valueEvent, but aggregated client side: values are averaged and emitted as a
+ * single valueEvent per key every 2 minutes (and on flushMetrics before app quit). Note
+ * that only the window average survives - individual samples and sample counts do not.
+ *
+ * @param {EVENT_TYPE|string} eventType The kind of Event Type that needs to be logged- should be
+ * a js var compatible string.
+ * @param {string} eventCategory The kind of Event Category that
+ * needs to be logged- should be a js var compatible string
+ * @param {string} eventSubCategory The kind of Event Sub Category that
+ * needs to be logged- should be a js var compatible string
+ * @param {number} value
+ * @type {function}
+ */
+ function valueEventBatched(eventType, eventCategory, eventSubCategory, value) {
+ const key = eventType + BATCH_KEY_SEPARATOR + eventCategory + BATCH_KEY_SEPARATOR + eventSubCategory;
+ const entry = _batchedValues.get(key);
+ if(entry){
+ entry.sum += Number(value);
+ entry.count++;
+ } else {
+ _batchedValues.set(key, {sum: Number(value), count: 1});
+ }
+ }
+
+ function _flushBatchedEvents() {
+ for(const [key, count] of _batchedCounts){
+ const parts = key.split(BATCH_KEY_SEPARATOR);
+ countEvent(parts[0], parts[1], parts[2], count);
+ }
+ _batchedCounts.clear();
+ for(const [key, entry] of _batchedValues){
+ const parts = key.split(BATCH_KEY_SEPARATOR);
+ valueEvent(parts[0], parts[1], parts[2], Math.round(entry.sum / entry.count));
+ }
+ _batchedValues.clear();
+ }
+ setInterval(_flushBatchedEvents, BATCH_FLUSH_MS);
+
function setDisabled(shouldDisable) {
Phoenix._setHealthTrackingDisabled(shouldDisable);
disabled = shouldDisable;
@@ -472,6 +540,7 @@ define(function (require, exports, module) {
*/
async function flushMetrics() {
try{
+ _flushBatchedEvents();
if(Phoenix.isNativeApp) {
_sendQueuedTauriGAEvents();
}
@@ -535,6 +604,8 @@ define(function (require, exports, module) {
exports.clearAuditData = clearAuditData;
exports.countEvent = countEvent;
exports.valueEvent = valueEvent;
+ exports.countEventBatched = countEventBatched;
+ exports.valueEventBatched = valueEventBatched;
exports.logPerformanceTime = logPerformanceTime;
exports.flushMetrics = flushMetrics;
exports.getRangeName = getRangeName;
diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js
index 8ea39f8053..ea06e3326b 100644
--- a/src/view/DefaultPanelView.js
+++ b/src/view/DefaultPanelView.js
@@ -31,7 +31,8 @@ define(function (require, exports, module) {
CommandManager = require("command/CommandManager"),
Strings = require("strings"),
WorkspaceManager = require("view/WorkspaceManager"),
- PanelView = require("view/PanelView");
+ PanelView = require("view/PanelView"),
+ Metrics = require("utils/Metrics");
/**
* Descriptors for each launcher button.
@@ -182,6 +183,7 @@ define(function (require, exports, module) {
.attr("title", Strings.BOTTOM_PANEL_DEFAULT_TITLE);
$drawerBtn.on("click", function () {
+ Metrics.countEvent(Metrics.EVENT_TYPE.UI, "toolbarBtn", "appDrawer");
// Design mode collapses the editor and stretches live preview, which
// leaves no room for the bottom tools panel. Exit design mode first
// so the panel has somewhere to go.
diff --git a/test/spec/Extn-JSONSupport-integ-test.js b/test/spec/Extn-JSONSupport-integ-test.js
index 3a82eb48cb..3562fa4095 100644
--- a/test/spec/Extn-JSONSupport-integ-test.js
+++ b/test/spec/Extn-JSONSupport-integ-test.js
@@ -182,16 +182,27 @@ define(function (require, exports, module) {
async function () {
const PreferencesManager = testWindow.brackets.test.PreferencesManager;
const LSPClient = testWindow.require("languageTools/LSPClient");
+ const Metrics = testWindow.brackets.getModule("utils/Metrics");
+ const audit = Metrics.getLoggedDataForAudit();
+ function auditSum(key) {
+ const entry = audit.get(key);
+ return (entry && entry.sum) || 0;
+ }
await _openFile("broken.json");
await awaitsFor(function () {
return LSPClient.isLintingProviderActive("json");
}, "json server to be active", 30000);
+ const startsBefore = auditSum("lsp.srv.Start.json"),
+ prefOffBefore = auditSum("lsp.srv.PrefOff.json");
PreferencesManager.set("codeIntelligence.json", false);
try {
await awaitsFor(function () {
return !LSPClient.isLintingProviderActive("json");
}, "json server to stop on pref off", 15000);
+ // greater-than, not exactly +1: incidental server events (a crash
+ // auto-restart, a scope reload) must not flake the spec
+ expect(auditSum("lsp.srv.PrefOff.json")).toBeGreaterThan(prefOffBefore);
} finally {
// restore even on failure so later suites see a healthy default
PreferencesManager.set("codeIntelligence.json", true);
@@ -199,6 +210,8 @@ define(function (require, exports, module) {
await awaitsFor(function () {
return LSPClient.isLintingProviderActive("json");
}, "json server to restart on pref on", 45000);
+ // re-enable is a return to the default - no pref metric, but the restart counts
+ expect(auditSum("lsp.srv.Start.json")).toBeGreaterThan(startsBefore);
}, 90000);
});
});
diff --git a/test/spec/Metrics-test.js b/test/spec/Metrics-test.js
index f77f4b71e6..6dccf1aa63 100644
--- a/test/spec/Metrics-test.js
+++ b/test/spec/Metrics-test.js
@@ -65,5 +65,33 @@ define(function (require, exports, module) {
data = Metrics.getLoggedDataForAudit();
expect(data.size < 2050).toBeTrue();
});
+
+ it("should aggregate batched count events and emit the sum on flushMetrics", async function () {
+ Metrics.init();
+ let data = Metrics.getLoggedDataForAudit();
+ data.clear();
+ Metrics.countEventBatched("typeb", "cat", "sub");
+ Metrics.countEventBatched("typeb", "cat", "sub");
+ Metrics.countEventBatched("typeb", "cat", "sub");
+ // nothing reaches the pipeline until a flush
+ expect(data.get("typeb.cat.sub")).toBeUndefined();
+ await Metrics.flushMetrics();
+ expect(data.get("typeb.cat.sub")).toEqual({ eventType: 'count', sum: 3, count: 1 });
+ // the window was consumed - a second flush must not double count
+ await Metrics.flushMetrics();
+ expect(data.get("typeb.cat.sub")).toEqual({ eventType: 'count', sum: 3, count: 1 });
+ });
+
+ it("should aggregate batched value events and emit the window average on flushMetrics", async function () {
+ Metrics.init();
+ let data = Metrics.getLoggedDataForAudit();
+ data.clear();
+ Metrics.valueEventBatched("typebv", "cat", "sub", 10);
+ Metrics.valueEventBatched("typebv", "cat", "sub", 30);
+ expect(data.get("typebv.cat.sub")).toBeUndefined();
+ await Metrics.flushMetrics();
+ // one value event carrying the average of the window (20), not two events
+ expect(data.get("typebv.cat.sub")).toEqual({ eventType: 'val', sum: 20, count: 1 });
+ });
});
});
diff --git a/tracking-repos.json b/tracking-repos.json
index cb87e9987d..8b3d419324 100644
--- a/tracking-repos.json
+++ b/tracking-repos.json
@@ -1,5 +1,5 @@
{
"phoenixPro": {
- "commitID": "f0774ecdb6a1ad14330dae7c4482a7c77c385f60"
+ "commitID": "5b52e051868bfb97e167e3c0233733443cd68701"
}
}