Skip to content
7 changes: 7 additions & 0 deletions .chronus/changes/tel-improve-compiler-2026-6-1-19-0-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Wrapped LSP server handlers with `wrapUnhandledError` to preserve server-side stack traces in error messages forwarded to the client. Previously, the JSON-RPC layer discarded the original stack trace, making unhandled errors in telemetry opaque.
7 changes: 7 additions & 0 deletions .chronus/changes/tel-improve-vscode-2026-6-1-19-0-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "typespec-vscode"
---

Improved telemetry instrumentation for `install-global-compiler-cli`, `preview-openapi3`, `start-server`, and `server-path-changed` events by adding missing `lastStep` tracking and error detail logging. Added actionable error message when compiler is found but neither `node` nor `tsp` is available on PATH, guiding users to fix common nvm/fnm/volta configuration issues.
117 changes: 87 additions & 30 deletions packages/compiler/src/server/serverlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,44 +182,101 @@ export function createServer(
let isInitialized = false;
let pendingMessages: ServerLog[] = [];

/**
* Wraps an LSP handler to preserve the server-side error details when it crashes.
*
* By default, the JSON-RPC layer (vscode-languageserver) catches handler errors and
* creates a new ResponseError using only `error.message`, discarding the original stack
* trace. On the client side, the telemetry framework then captures this as an unhandled
* error, but the `unhandled_error_stack` only shows the client-side message handling code:
*
* ```
* Error: Request textDocument/hover failed with message: Cannot read properties of undefined (reading 'kind')
* at handleResponse (extension.cjs:2104:40) // <-- client-side LSP message handler
* at handleMessage (extension.cjs:1914:11)
* at processMessageQueue (extension.cjs:1929:13)
* at Immediate.<anonymous> (extension.cjs:1905:11)
* ```
*
* The actual server-side crash location (e.g., in the checker or parser) is completely lost.
*
* This wrapper catches the error first and re-throws a new Error whose message includes
* the full original error details (stack trace for Error instances, String() for others).
* The JSON-RPC layer then forwards this enriched message to the client, so the
* telemetry `unhandled_error_message` will contain the server-side crash location:
*
* ```
* [getHover] TypeError: Cannot read properties of undefined (reading 'kind')
* at Checker.getTypeForNode (checker.ts:1234:15) // <-- actual crash location
* at getHover (serverlib.ts:826:52)
* ...
* ```
*/
function wrapUnhandledError<T extends (...args: any[]) => any>(name: string, fn: T): T {
return (async (...args: any[]) => {
try {
return await fn(...args);
} catch (e) {
if (e instanceof Error) {
const detail = e.stack ? `${e.message}\n${e.stack}` : e.message;
throw new Error(`[${name}] ${detail}`, { cause: e });
} else if (typeof e === "string") {
throw new Error(`[${name}] ${e}`, { cause: e });
} else if (typeof e === "object" && e !== null) {
let detail: string;
try {
detail = JSON.stringify(e);
} catch {
throw e;
}
throw new Error(`[${name}] ${detail}`, { cause: e });
}
throw e;
}
}) as T;
}

return {
get pendingMessages() {
return pendingMessages;
},
get workspaceFolders() {
return workspaceFolders;
},
compile,
initialize,
initialized,
workspaceFoldersChanged,
watchedFilesChanged,
formatDocument,
gotoDefinition,
documentClosed,
documentOpened,
complete,
findReferences,
findDocumentHighlight,
prepareRename,
rename,
renameFiles,
getSemanticTokens: getSemanticTokensForDocument,
buildSemanticTokens,
checkChange,
getFoldingRanges,
getHover,
getSignatureHelp,
getDocumentSymbols,
getCodeActions,
resolveCodeAction,
compile: wrapUnhandledError("compile", compile),
initialize: wrapUnhandledError("initialize", initialize),
initialized: wrapUnhandledError("initialized", initialized),
workspaceFoldersChanged: wrapUnhandledError("workspaceFoldersChanged", workspaceFoldersChanged),
watchedFilesChanged: wrapUnhandledError("watchedFilesChanged", watchedFilesChanged),
formatDocument: wrapUnhandledError("formatDocument", formatDocument),
gotoDefinition: wrapUnhandledError("gotoDefinition", gotoDefinition),
documentClosed: wrapUnhandledError("documentClosed", documentClosed),
documentOpened: wrapUnhandledError("documentOpened", documentOpened),
complete: wrapUnhandledError("complete", complete),
findReferences: wrapUnhandledError("findReferences", findReferences),
findDocumentHighlight: wrapUnhandledError("findDocumentHighlight", findDocumentHighlight),
prepareRename: wrapUnhandledError("prepareRename", prepareRename),
rename: wrapUnhandledError("rename", rename),
renameFiles: wrapUnhandledError("renameFiles", renameFiles),
getSemanticTokens: wrapUnhandledError("getSemanticTokens", getSemanticTokensForDocument),
buildSemanticTokens: wrapUnhandledError("buildSemanticTokens", buildSemanticTokens),
checkChange: wrapUnhandledError("checkChange", checkChange),
getFoldingRanges: wrapUnhandledError("getFoldingRanges", getFoldingRanges),
getHover: wrapUnhandledError("getHover", getHover),
getSignatureHelp: wrapUnhandledError("getSignatureHelp", getSignatureHelp),
getDocumentSymbols: wrapUnhandledError("getDocumentSymbols", getDocumentSymbols),
getCodeActions: wrapUnhandledError("getCodeActions", getCodeActions),
resolveCodeAction: wrapUnhandledError("resolveCodeAction", resolveCodeAction),
log,
reportDiagnostics,

getInitProjectContext,
validateInitProjectTemplate,
initProject,
internalCompile,
reportDiagnostics: wrapUnhandledError("reportDiagnostics", reportDiagnostics),

getInitProjectContext: wrapUnhandledError("getInitProjectContext", getInitProjectContext),
validateInitProjectTemplate: wrapUnhandledError(
"validateInitProjectTemplate",
validateInitProjectTemplate,
),
initProject: wrapUnhandledError("initProject", initProject),
internalCompile: wrapUnhandledError("internalCompile", internalCompile),
};

async function initialize(params: InitializeParams): Promise<InitializeResult> {
Expand Down
4 changes: 4 additions & 0 deletions packages/typespec-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export async function activate(context: ExtensionContext) {
await telemetryClient.doOperationWithTelemetry(
TelemetryEventName.ServerPathSettingChanged,
async (tel) => {
tel.lastStep = "Recreate LSP client for path change";
return await recreateLSPClient(context, tel.activityId);
},
undefined,
Expand Down Expand Up @@ -316,6 +317,7 @@ export async function activate(context: ExtensionContext) {
}
// client will be undefined only when we can't find compiler locally or globally
// otherwise, the client should always be created though the start command may fail which is a different case
ssTel.lastStep = "Compiler not found (prompting to install)";
const choice: "Yes" | "Ignore" | undefined = await vscode.window.showWarningMessage(
"No TypeSpec compiler found which is required to start TypeSpec language server. Do you want to install TypeSpec compiler?",
"Yes",
Expand Down Expand Up @@ -355,6 +357,8 @@ export async function activate(context: ExtensionContext) {
{ showPopup: true },
);
ssTel.lastStep = "Failed to install TypeSpec compiler.";
} else {
ssTel.lastStep = "Install TypeSpec compiler cancelled.";
}
return installResult.code;
},
Expand Down
29 changes: 25 additions & 4 deletions packages/typespec-vscode/src/tsp-executable-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SettingName } from "./types.js";
import {
checkInstalledExecutable,
checkInstalledNode,
checkInstalledTspCli,
isFile,
loadModule,
useShellInExec,
Expand Down Expand Up @@ -159,12 +160,32 @@ export async function resolveTypeSpecServer(
});
return { command: "node", args: [serverPath, ...args], options };
} else {
// otherwise the local compiler should be installed by standalone tsp cli
logger.debug("Start tsp server using standalone tsp cli");
const tspCliPath = await checkInstalledTspCli();
if (tspCliPath.length > 0) {
logger.debug("Start tsp server using standalone tsp cli");
telemetryClient.logOperationDetailTelemetry(activityId, {
compilerStartType: "standalone-tsp-cli",
});
return { command: "tsp", args: ["--server", serverPath, ...args], options };
}
// Neither node nor tsp is on PATH.
logger.error(
[
`TypeSpec compiler was found at '${serverPath}', but it cannot be started because neither 'node' nor 'tsp' is available in PATH.`,
"This commonly happens when Node.js is installed via a version manager (nvm, fnm, volta) whose PATH is not inherited by VS Code.",
"To fix this, try one of the following:",
" - Launch VS Code from a terminal where 'node' is available (e.g. run 'code .' after activating nvm).",
" - Set the 'typespec.tsp-server.path' setting to the full path of your tsp-server.js file.",
" - Install Node.js system-wide so it's available to all processes.",
].join("\n"),
[],
{ showPopup: true, showOutput: true },
);
telemetryClient.logOperationDetailTelemetry(activityId, {
compilerStartType: "standalone-tsp-cli",
compilerStartType: "not-available",
error: "Neither node nor tsp is available in PATH. Compiler found but cannot be started.",
});
return { command: "tsp", args: ["--server", serverPath, ...args], options };
return undefined;
}
}

Expand Down
27 changes: 21 additions & 6 deletions packages/typespec-vscode/src/vscode-cmd/install-tsp-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { inspect } from "util";
import logger from "../log/logger.js";
import telemetryClient from "../telemetry/telemetry-client.js";
import { TelemetryEventName } from "../telemetry/telemetry-event.js";
Expand All @@ -11,6 +12,7 @@ export async function installCompilerGlobally(
TelemetryEventName.InstallGlobalCompilerCli,
async (tel) => {
const showPopup = args?.silentMode !== true;
tel.lastStep = "Call installCompilerWithUi";
const result = await installCompilerWithUi(
{
confirmNeeded: args?.confirm !== false,
Expand All @@ -20,13 +22,26 @@ export async function installCompilerGlobally(
[] /*localPath, empty for global*/,
);
if (result.code === ResultCode.Success) {
tel.lastStep = "Compiler installed successfully";
logger.info(`Compiler installed successfully`, [], { showPopup });
} else if (result.code === ResultCode.Fail || result.code === ResultCode.Timeout) {
logger.error(
`Installing compiler ${result.code === ResultCode.Fail ? "failed" : "timeout"}. Please check previous logs for details`,
[],
{ showPopup },
);
} else if (result.code === ResultCode.Cancelled) {
tel.lastStep = "User cancelled installation";
} else if (result.code === ResultCode.Timeout) {
tel.lastStep = "Installation timeout";
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
error: `Installing compiler globally timeout`,
});
logger.error(`Installing compiler timeout. Please check previous logs for details`, [], {
showPopup,
});
} else if (result.code === ResultCode.Fail) {
tel.lastStep = "Installation failed";
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
error: `Installing compiler globally failed: ${inspect(result.details)}`,
});
logger.error(`Installing compiler failed. Please check previous logs for details`, [], {
showPopup,
});
}
return result;
},
Expand Down
52 changes: 28 additions & 24 deletions packages/typespec-vscode/src/vscode-cmd/openapi3-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getBaseFileName, getDirectoryPath, joinPaths } from "../path-utils.js";
import telemetryClient from "../telemetry/telemetry-client.js";
import { OperationTelemetryEvent } from "../telemetry/telemetry-event.js";
import { TspLanguageClient } from "../tsp-language-client.js";
import { ResultCode } from "../types.js";
import { Result, ResultCode } from "../types.js";
import { getEntrypointTspFile, TraverseMainTspFileInWorkspace } from "../typespec-utils.js";
import { createTempDir, throttle } from "../utils.js";

Expand Down Expand Up @@ -134,15 +134,13 @@ async function loadOpenApi3PreviewPanel(
});
panel.reveal();
} else {
const getOpenApi3OutputFilePath = async (
selectOutput: boolean,
): Promise<string | undefined> => {
const getOpenApi3OutputFilePath = async (selectOutput: boolean): Promise<Result<string>> => {
return await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Loading OpenAPI3 files...",
},
async (): Promise<string | undefined> => {
async (): Promise<Result<string>> => {
const srcFolder = getDirectoryPath(mainTspFile);
const outputFolder = await getOutputFolder(mainTspFile, tmpRoot);
if (!outputFolder) {
Expand All @@ -153,7 +151,7 @@ async function loadOpenApi3PreviewPanel(
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
error: "Failed to create temporary folder for OpenAPI3 files",
});
return undefined;
return { code: ResultCode.Fail };
}
await clearOutputFolder(outputFolder);

Expand All @@ -163,27 +161,32 @@ async function loadOpenApi3PreviewPanel(
"Failed to generate OpenAPI3 files.",
result?.stderr ? [result.stderr] : [],
);
return;
} else {
return await selectAndGetOpenApi3FilePath(
mainTspFile,
outputFolder,
selectOutput,
context,
);
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
error: `Failed to compile OpenAPI3: exitCode=${result?.exitCode ?? "N/A"}, stderr=${result?.stderr ?? "N/A"}`,
});
return { code: ResultCode.Fail };
}
const filePath = await selectAndGetOpenApi3FilePath(
mainTspFile,
outputFolder,
selectOutput,
context,
);
if (filePath === undefined) {
return { code: ResultCode.Cancelled };
}
return { code: ResultCode.Success, value: filePath };
},
);
};

const filePath = await getOpenApi3OutputFilePath(true);
if (filePath === undefined) {
telemetryClient.logOperationDetailTelemetry(tel.activityId, {
error: "Failed to get generated OpenAPI3 file",
});
tel.lastStep = "Get OpenAPI3 output";
return ResultCode.Cancelled;
const outputResult = await getOpenApi3OutputFilePath(true);
if (outputResult.code !== ResultCode.Success) {
tel.lastStep =
outputResult.code === ResultCode.Fail ? "Compile OpenAPI3 failed" : "Get OpenAPI3 output";
return outputResult.code;
}
const filePath = outputResult.value;

const panel = vscode.window.createWebviewPanel(
"webview",
Expand All @@ -199,11 +202,11 @@ async function loadOpenApi3PreviewPanel(

const watch = vscode.workspace.createFileSystemWatcher("**/*.{tsp}");
const throttledChangeHandler = throttle(async () => {
const outputFilePath = await getOpenApi3OutputFilePath(false);
if (outputFilePath) {
const refreshResult = await getOpenApi3OutputFilePath(false);
if (refreshResult.code === ResultCode.Success) {
void panel.webview.postMessage({
command: "load",
param: panel.webview.asWebviewUri(vscode.Uri.file(outputFilePath)).toString(),
param: panel.webview.asWebviewUri(vscode.Uri.file(refreshResult.value)).toString(),
});
}
}, 1000);
Expand Down Expand Up @@ -233,6 +236,7 @@ async function loadOpenApi3PreviewPanel(

loadHtml(context.extensionUri, panel);
}
tel.lastStep = "Preview panel opened";
return ResultCode.Success;
}

Expand Down
Loading