Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d5a67c2
Improve Aspire VS Code extension funnel
adamint Jun 4, 2026
66b6563
Keep launch telemetry best-effort
adamint Jun 4, 2026
18a7f6e
Update VS Code extension README assets
adamint Jun 5, 2026
c13f879
Fix AppHost discovery tests on Windows
adamint Jun 5, 2026
e33c5ed
Apply suggestion from @adamint
adamint Jun 5, 2026
4c37ce8
Fix VS Code extension README link text
adamint Jun 5, 2026
cb130c0
Merge main into vscode extension growth
adamint Jun 5, 2026
f332fdf
Update VS Code extension funnel screenshots
adamint Jun 11, 2026
1b8d14f
Merge remote-tracking branch 'upstream/main' into fleet/pr-lane-vscod…
adamint Jun 17, 2026
15cbf25
Use real VS Code captures for extension walkthrough
adamint Jun 17, 2026
deadb2f
Merge remote-tracking branch 'upstream/main' into fleet/pr-lane-vscod…
adamint Jun 18, 2026
d379ce9
Replace extension README media with real captures
adamint Jun 18, 2026
45a5f7b
Harden VS Code telemetry dimensions
adamint Jun 18, 2026
c6a56c8
Fix canceled AppHost launch telemetry
adamint Jun 18, 2026
12ef868
Improve VS Code extension funnel visuals
adamint Jun 20, 2026
4254d2e
Merge upstream main into VS Code funnel PR
adamint Jun 20, 2026
8d12f64
Polish VS Code extension README screenshots
adamint Jun 20, 2026
9b37e2e
Refine VS Code extension README visuals
adamint Jun 20, 2026
cea1935
Improve extension README visuals
adamint Jun 20, 2026
9eabd90
Merge remote-tracking branch 'upstream/main' into pr-17898-review-fixes
Jun 20, 2026
b10ba7d
Add AppHosts view telemetry tests
Jun 20, 2026
9f0862e
Update VS Code extension screenshot evidence
Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,27 +60,38 @@ Open your Aspire project in VS Code, or create one with **Aspire: New Aspire pro

There's also a built-in walkthrough at **Help → Get Started → Get started with Aspire** that covers the basics step by step.

The core flow is:

1. Open an Aspire repo or create a starter with **Aspire: New Aspire project**.
2. Let the Aspire view discover the AppHost.
3. Press **F5**, or use **Run Aspire apphost** / **Debug Aspire apphost** from the editor.
4. Inspect resources in the Aspire view and open the dashboard when you need logs, traces, metrics, or endpoint URLs.

Learn more in the [Aspire VS Code extension documentation](https://aspire.dev/get-started/aspire-vscode-extension/).

---

## Running and Debugging

### Launch configuration

Add an entry to `.vscode/launch.json` pointing at your apphost project:
Add an entry to `.vscode/launch.json` pointing at your apphost:

```json
{
"type": "aspire",
"request": "launch",
"name": "Aspire: Launch MyAppHost",
"program": "${workspaceFolder}/MyAppHost/MyAppHost.csproj"
"name": "Aspire: Launch TypeScript starter",
"program": "${workspaceFolder}/AppHost/apphost.mts"
}
```

When you hit **F5**, the extension builds the apphost, starts all the resources (services, containers, databases) in the right order, hooks up debuggers based on each service's language, and opens the dashboard.

You can also right-click an `apphost.cs`, `apphost.ts`, or `apphost.js` file in the Explorer and pick **Run Aspire apphost** or **Debug Aspire apphost**.

![VS Code running and debugging an Aspire AppHost with resource debug sessions.](./resources/vscode-extension-debug-session.png)

### Deploy, publish, and pipeline steps

The `command` property in the launch config lets you do more than just run:
Expand All @@ -93,8 +104,8 @@ The `command` property in the launch config lets you do more than just run:
{
"type": "aspire",
"request": "launch",
"name": "Aspire: Deploy MyAppHost",
"program": "${workspaceFolder}/MyAppHost/MyAppHost.csproj",
"name": "Aspire: Deploy TypeScript starter",
"program": "${workspaceFolder}/AppHost/apphost.mts",
"command": "deploy"
}
```
Expand Down Expand Up @@ -129,15 +140,17 @@ The extension adds an **Aspire** panel to the Activity Bar. It shows a live tree

Right-click a resource to start, stop, or restart it, view its logs, run resource-specific commands, or open the dashboard.

![Aspire view discovering a workspace AppHost and showing resources with live state.](./resources/vscode-extension-apphost-view.png)

---

## The Aspire Dashboard

The dashboard gives you a live view of your running app — all your resources and their health, endpoint URLs, console logs from every service, structured logs (via OpenTelemetry), distributed traces across services, and metrics.

![Aspire Dashboard showing running resources](https://raw.githubusercontent.com/microsoft/aspire/main/extension/resources/aspire-dashboard-dark.png)
![Aspire Dashboard showing running resources](./resources/aspire-dashboard-dark.png)

It opens automatically when you start your app. You can pick which browser it uses with the `aspire.dashboardBrowser` setting — system default browser, or Chrome, Edge, or Firefox as a debug session. When using a debug browser, the `aspire.closeDashboardOnDebugEnd` setting controls whether it closes automatically when you stop debugging. Firefox also requires the [Firefox Debugger](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug) extension.
It opens automatically when you start your app. You can also open it from the Aspire view or CodeLens if you closed the tab. Pick which browser it uses with the `aspire.dashboardBrowser` setting — system default browser, or Chrome, Edge, or Firefox as a debug session. When using a debug browser, the `aspire.closeDashboardOnDebugEnd` setting controls whether it closes automatically when you stop debugging. Firefox also requires the [Firefox Debugger](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug) extension.

---

Expand Down
Binary file modified extension/resources/aspire-dashboard-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AspireDebugConfigurationProvider implements vscode.DebugConfigurati

async resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise<vscode.DebugConfiguration | null | undefined> {
// Check if CLI is available before starting debug session
const result = await checkCliAvailableOrRedirect();
const result = await checkCliAvailableOrRedirect('debug_gate');
if (!result.available) {
return undefined; // Cancel the debug session
}
Expand Down
10 changes: 2 additions & 8 deletions extension/src/debugger/AspireDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import os from "os";
import { EnvironmentVariables } from "../utils/environment";
import { sendTelemetryEvent } from "../utils/telemetry";
import { classifyAppHostPath, classifyAppHostDirectory } from "../utils/appHostLanguage";
import { bucketAspireCommand } from "../utils/telemetryBuckets";
import type { AspireDebugConsoleOutputEvent } from "../types/extensionApi";

export type DashboardBrowserType = 'openExternalBrowser' | 'integratedBrowser' | 'debugChrome' | 'debugEdge' | 'debugFirefox';
Expand Down Expand Up @@ -154,18 +155,11 @@ export class AspireDebugSession implements vscode.DebugAdapter {
? classifyAppHostDirectory(appHostPath)
: classifyAppHostPath(appHostPath);
this._appHostModeAtLaunch = noDebug ? 'run' : 'debug';
// `command` originates in the user's launch.json and is typed in the
// contributing extension surface as AspireCommandType ('run'|'deploy'|
// 'publish'|'do'), but launch.json is freeform JSON — a typo or custom
// value would otherwise leak verbatim into telemetry. Clamp to the known
// set so the dimension stays bounded.
const knownCommands: ReadonlySet<string> = new Set(['run', 'deploy', 'publish', 'do']);
const commandForTelemetry = knownCommands.has(command) ? command : 'other';
sendTelemetryEvent('debug/apphost/start', {
mode: this._appHostModeAtLaunch,
apphost_language: this._appHostLanguageAtLaunch,
apphost_is_directory: appHostIsDirectory ? 'true' : 'false',
command: commandForTelemetry,
command: bucketAspireCommand(command),
});

// For 'do' with an explicit step (old CLI fallback), pass it as a positional argument
Expand Down
24 changes: 22 additions & 2 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { publishCommand } from './commands/publish';
import { doCommand } from './commands/do';
import { cliNotAvailable, dismissLabel, errorMessage, openCliInstallInstructions } from './loc/strings';
import { extensionLogOutputChannel } from './utils/logging';
import { initializeTelemetry, isCommandCancellation, withCommandTelemetry } from './utils/telemetry';
import { initializeTelemetry, isCommandCancellation, sendTelemetryEvent, withCommandTelemetry } from './utils/telemetry';
import { MeaningfulEngagementReporter } from './utils/meaningfulEngagement';
import { AspireDebugAdapterDescriptorFactory } from './debugger/AspireDebugAdapterDescriptorFactory';
import { AspireDebugConfigurationProvider } from './debugger/AspireDebugConfigurationProvider';
Expand Down Expand Up @@ -53,6 +53,12 @@ export async function activate(context: vscode.ExtensionContext) {
const gitCommitSha = readGitCommitSha(context);
extensionLogOutputChannel.info(`Activating Aspire extension (commit: ${gitCommitSha})`);
initializeTelemetry(context);
sendTelemetryEvent('extension/activated', {
workspace_open: vscode.workspace.workspaceFolders?.length ? 'true' : 'false',
extension_mode: getExtensionModeForTelemetry(context.extensionMode),
}, {
workspace_folders: vscode.workspace.workspaceFolders?.length ?? 0,
});

const terminalProvider = new AspireTerminalProvider(context.subscriptions);
const testRunSessionManager = new TestRunSessionManager();
Expand Down Expand Up @@ -391,6 +397,19 @@ export function deactivate() {
aspireExtensionContext.dispose();
}

function getExtensionModeForTelemetry(mode: vscode.ExtensionMode): string {
switch (mode) {
case vscode.ExtensionMode.Production:
return 'production';
case vscode.ExtensionMode.Development:
return 'development';
case vscode.ExtensionMode.Test:
return 'test';
default:
return 'unknown';
}
}

async function tryExecuteCommand(commandName: string, terminalProvider: AspireTerminalProvider, command: (terminalProvider: AspireTerminalProvider) => Promise<void>): Promise<void> {
try {
await withCommandTelemetry(commandName, async () => {
Expand All @@ -405,14 +424,15 @@ async function tryExecuteCommand(commandName: string, terminalProvider: AspireTe
throw new vscode.CancellationError();
}

const result = await checkCliAvailableOrRedirect();
const result = await checkCliAvailableOrRedirect('command_gate');
if (!result.available) {
// The command body never ran — the user was redirected to install the
// CLI. Throwing a cancellation makes withCommandTelemetry record this
// as `canceled` rather than a false `success`, and the catch below
// suppresses the error toast (the redirect already informed the user).
throw new vscode.CancellationError();
}

}

await command(terminalProvider);
Expand Down
61 changes: 58 additions & 3 deletions extension/src/services/AppHostLaunchService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as path from 'path';
import * as fs from 'fs';
import * as vscode from 'vscode';
import { AspireCommandType, AspireExtendedDebugConfiguration } from '../dcp/types';
import { classifyAppHostDirectory, classifyAppHostPath } from '../utils/appHostLanguage';
import { classifyError, isCommandCancellation, sendTelemetryEvent, type EventProperties } from '../utils/telemetry';
import { bucketAspireCommand } from '../utils/telemetryBuckets';

function getComparisonKey(value: string): string {
return process.platform === 'win32' ? value.toLowerCase() : value;
Expand Down Expand Up @@ -132,6 +136,10 @@ export class AppHostLaunchService implements vscode.Disposable {
* @param doStep Optional step name for the 'do' command.
*/
async launch(appHostPath: string, command: AspireCommandType, noDebug: boolean, doStep?: string): Promise<void> {
const startTime = Date.now();
const executionSuppressed = isE2eDebugLaunchSuppressed();
const telemetryProperties = getLaunchTelemetryProperties(appHostPath, command, noDebug, executionSuppressed);

// Track launching state before awaiting startDebugging so the tree shows "Starting..."
// immediately. We must clear this state if startDebugging returns false (debug adapter
// rejected, no provider matched, user cancelled) or throws — otherwise no terminate
Expand All @@ -153,32 +161,79 @@ export class AppHostLaunchService implements vscode.Disposable {
config.step = doStep;
}

const executionSuppressed = isE2eDebugLaunchSuppressed();
this._onDidRequestLaunch.fire({
appHostPath,
command,
noDebug,
doStep,
executionSuppressed,
});

if (executionSuppressed) {
this.clearLaunching(appHostPath);
sendTelemetryEvent('apphost/launch/result', {
...telemetryProperties,
outcome: 'suppressed',
}, {
duration_ms: Date.now() - startTime,
});
return;
}

try {
const started = await vscode.debug.startDebugging(undefined, config);
if (!started) {
throw new Error(`VS Code did not start the Aspire ${command} session for ${vscode.workspace.asRelativePath(appHostPath)}.`);
// A false result means VS Code declined the launch before the
// debug session started (for example, no provider matched or
// an adapter gate rejected it). Surface it as an error so the
// tree command path does not silently swallow a real launch
// failure while still clearing the temporary "Starting..." state.
const error = new Error(`VS Code did not start the Aspire ${command} session for ${vscode.workspace.asRelativePath(appHostPath)}.`);
error.name = 'StartDebuggingDeclined';
throw error;
}
sendTelemetryEvent('apphost/launch/result', {
...telemetryProperties,
outcome: 'success',
}, {
duration_ms: Date.now() - startTime,
});
} catch (err) {
this.clearLaunching(appHostPath);
const canceled = isCommandCancellation(err);
const properties: EventProperties<'apphost/launch/result'> = {
...telemetryProperties,
outcome: canceled ? 'canceled' : 'error',
};
if (!canceled) {
properties.error_kind = classifyError(err);
}
sendTelemetryEvent('apphost/launch/result', properties, {
duration_ms: Date.now() - startTime,
});
throw err;
}
}
}

function getLaunchTelemetryProperties(appHostPath: string, command: AspireCommandType, noDebug: boolean, executionSuppressed: boolean) {
const isDirectory = isDirectoryForTelemetry(appHostPath);
return {
mode: noDebug ? 'run' : 'debug',
command: bucketAspireCommand(command),
apphost_language: isDirectory ? classifyAppHostDirectory(appHostPath) : classifyAppHostPath(appHostPath),
execution_suppressed: executionSuppressed ? 'true' : 'false',
};
}
Comment thread
adamint marked this conversation as resolved.

function isDirectoryForTelemetry(appHostPath: string): boolean {
try {
return fs.statSync(appHostPath, { throwIfNoEntry: false })?.isDirectory() === true;
}
catch {
return false;
}
}

function isE2eDebugLaunchSuppressed(): boolean {
return process.env.ASPIRE_EXTENSION_E2E_ENABLE_BRIDGE === 'true' &&
!!process.env.ASPIRE_EXTENSION_E2E_STATE_FILE &&
Expand Down
2 changes: 1 addition & 1 deletion extension/src/test/appHostDataRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1680,7 +1680,7 @@ suite('AppHostDataRepository', () => {
assert.strictEqual(repository.viewMode, 'workspace');
assert.strictEqual(repository.workspaceAppHostPath, '/workspace/apps/Store/AppHost.csproj');
assert.strictEqual(repository.workspaceAppHostName, 'AppHost.csproj');
assert.strictEqual(repository.workspaceAppHostDescription, 'Workspace view selected because aspire ls found one buildable AppHost.');
assert.strictEqual(repository.workspaceAppHostDescription, 'Workspace view selected because aspire ls found one buildable C# AppHost.');
} finally {
repository.dispose();
workspaceFoldersStub.restore();
Expand Down
Loading
Loading