Skip to content

Commit f726148

Browse files
committed
feat: instrument startup telemetry
1 parent ae1920a commit f726148

8 files changed

Lines changed: 438 additions & 155 deletions

File tree

src/core/cliManager.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { errToStr } from "../api/api-helper";
1313
import * as pgp from "../pgp";
1414
import { withCancellableProgress, withOptionalProgress } from "../progress";
1515
import { isKeyringEnabled } from "../settings/cli";
16+
import { type CallerMeasurements } from "../telemetry/event";
17+
import { type TelemetryService } from "../telemetry/service";
1618
import { tempFilePath, toSafeHost } from "../util";
1719
import { vscodeProposed } from "../vscodeProposed";
1820

@@ -33,13 +35,16 @@ type ResolvedBinary =
3335
| { binPath: string; stat: Stats; source: "file-path" | "directory" }
3436
| { binPath: string; source: "not-found" };
3537

38+
type CliDownloadReason = "missing" | "version_mismatch";
39+
3640
export class CliManager {
3741
private readonly binaryLock: BinaryLock;
3842

3943
constructor(
4044
private readonly output: Logger,
4145
private readonly pathResolver: PathResolver,
4246
private readonly cliCredentialManager: CliCredentialManager,
47+
private readonly telemetry: TelemetryService,
4348
) {
4449
this.binaryLock = new BinaryLock(output);
4550
}
@@ -140,6 +145,8 @@ export class CliManager {
140145
);
141146

142147
let existingVersion: string | null = null;
148+
const downloadReason: CliDownloadReason =
149+
resolved.source === "not-found" ? "missing" : "version_mismatch";
143150
if (resolved.source !== "not-found") {
144151
this.output.debug(
145152
"Existing binary size is",
@@ -224,13 +231,25 @@ export class CliManager {
224231
latestVersion = latestParsedVersion;
225232
}
226233

227-
await this.performBinaryDownload(
228-
restClient,
229-
latestVersion,
230-
downloadBinPath,
231-
progressLogPath,
234+
const downloadMeasurements: CallerMeasurements = {};
235+
return await this.telemetry.trace(
236+
"cli.download",
237+
async () => {
238+
await this.performBinaryDownload(
239+
restClient,
240+
latestVersion,
241+
downloadBinPath,
242+
progressLogPath,
243+
);
244+
const downloadedStat = await cliUtils.stat(downloadBinPath);
245+
if (downloadedStat) {
246+
downloadMeasurements.sizeBytes = downloadedStat.size;
247+
}
248+
return this.renameToFinalPath(resolved, downloadBinPath);
249+
},
250+
{ reason: downloadReason },
251+
downloadMeasurements,
232252
);
233-
return await this.renameToFinalPath(resolved, downloadBinPath);
234253
} catch (error) {
235254
const fallback = await this.handleAnyBinaryFailure(
236255
error,
@@ -480,17 +499,19 @@ export class CliManager {
480499
"Skipping binary signature verification due to settings",
481500
);
482501
} else {
483-
await this.verifyBinarySignatures(client, tempFile, [
484-
// A signature placed at the same level as the binary. It must be
485-
// named exactly the same with an appended `.asc` (such as
486-
// coder-windows-amd64.exe.asc or coder-linux-amd64.asc).
487-
binSource + ".asc",
488-
// The releases.coder.com bucket does not include the leading "v",
489-
// and unlike what we get from buildinfo it uses a truncated version
490-
// with only major.minor.patch. The signature name follows the same
491-
// rule as above.
492-
`https://releases.coder.com/coder-cli/${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}/${binName}.asc`,
493-
]);
502+
await this.telemetry.trace("cli.verify", () =>
503+
this.verifyBinarySignatures(client, tempFile, [
504+
// A signature placed at the same level as the binary. It must be
505+
// named exactly the same with an appended `.asc` (such as
506+
// coder-windows-amd64.exe.asc or coder-linux-amd64.asc).
507+
binSource + ".asc",
508+
// The releases.coder.com bucket does not include the leading "v",
509+
// and unlike what we get from buildinfo it uses a truncated version
510+
// with only major.minor.patch. The signature name follows the same
511+
// rule as above.
512+
`https://releases.coder.com/coder-cli/${parsedVersion.major}.${parsedVersion.minor}.${parsedVersion.patch}/${binName}.asc`,
513+
]),
514+
);
494515
}
495516

496517
// Replace existing binary (handles both renames + Windows lock)

src/core/container.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ export class ServiceContainer implements vscode.Disposable {
5151
context.globalState,
5252
this.logger,
5353
);
54+
55+
const sessionId = newSessionId();
56+
const localJsonlSink = LocalJsonlSink.start(
57+
{
58+
baseDir: this.pathResolver.getTelemetryPath(),
59+
sessionId,
60+
},
61+
this.logger,
62+
);
63+
const session = buildSession(
64+
extractExtensionVersion(context.extension.packageJSON),
65+
sessionId,
66+
);
67+
this.telemetryService = new TelemetryService(
68+
session,
69+
[localJsonlSink],
70+
this.logger,
71+
);
72+
5473
// Circular ref: cliCredentialManager ↔ cliManager. The resolver
5574
// closure captures `this` by reference, so `this.cliManager` is
5675
// available when the closure is called (after construction).
@@ -75,6 +94,7 @@ export class ServiceContainer implements vscode.Disposable {
7594
this.logger,
7695
this.pathResolver,
7796
this.cliCredentialManager,
97+
this.telemetryService,
7898
);
7999
this.contextManager = new ContextManager(context);
80100
this.oauthCallback = new OAuthCallback(context.secrets, this.logger);
@@ -96,23 +116,6 @@ export class ServiceContainer implements vscode.Disposable {
96116
this.logger,
97117
);
98118

99-
const sessionId = newSessionId();
100-
const localJsonlSink = LocalJsonlSink.start(
101-
{
102-
baseDir: this.pathResolver.getTelemetryPath(),
103-
sessionId,
104-
},
105-
this.logger,
106-
);
107-
const session = buildSession(
108-
extractExtensionVersion(context.extension.packageJSON),
109-
sessionId,
110-
);
111-
this.telemetryService = new TelemetryService(
112-
session,
113-
[localJsonlSink],
114-
this.logger,
115-
);
116119
this.commandManager = new CommandManager(this.telemetryService);
117120
}
118121

src/extension.ts

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getErrorDetail, toError } from "./error/errorUtils";
1717
import { OAuthSessionManager } from "./oauth/sessionManager";
1818
import { Remote } from "./remote/remote";
1919
import { getRemoteSshExtension } from "./remote/sshExtension";
20+
import { ActivationTelemetry } from "./telemetry/startup";
2021
import { registerUriHandler } from "./uri/uriHandler";
2122
import { initVscodeProposed } from "./vscodeProposed";
2223
import { ChatPanelProvider } from "./webviews/chat/chatPanelProvider";
@@ -30,6 +31,22 @@ const MY_WORKSPACES_TREE_ID = "myWorkspaces";
3031
const ALL_WORKSPACES_TREE_ID = "allWorkspaces";
3132

3233
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
34+
const serviceContainer = new ServiceContainer(ctx);
35+
ctx.subscriptions.push(serviceContainer);
36+
const activationTelemetry = new ActivationTelemetry(
37+
serviceContainer.getTelemetryService(),
38+
);
39+
40+
await activationTelemetry.trace(() =>
41+
doActivate(ctx, serviceContainer, activationTelemetry),
42+
);
43+
}
44+
45+
async function doActivate(
46+
ctx: vscode.ExtensionContext,
47+
serviceContainer: ServiceContainer,
48+
activationTelemetry: ActivationTelemetry,
49+
): Promise<void> {
3350
// The Remote SSH extension's proposed APIs are used to override the SSH host
3451
// name in VS Code itself. It's visually unappealing having a lengthy name!
3552
//
@@ -39,6 +56,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
3956
// Cursor and VSCode are covered by ms remote, and the only other is windsurf for now
4057
// Means that vscodium is not supported by this for now
4158

59+
activationTelemetry.setPhase("remote_ssh_extension");
4260
const remoteSshExtension = getRemoteSshExtension();
4361

4462
let vscodeProposed: typeof vscode = vscode;
@@ -57,24 +75,31 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
5775
}
5876

5977
// Initialize the global vscodeProposed module for use throughout the extension
78+
activationTelemetry.setPhase("proposed_api_init");
6079
initVscodeProposed(vscodeProposed);
6180

62-
const serviceContainer = new ServiceContainer(ctx);
63-
ctx.subscriptions.push(serviceContainer);
64-
6581
const output = serviceContainer.getLogger();
6682
const mementoManager = serviceContainer.getMementoManager();
6783
const secretsManager = serviceContainer.getSecretsManager();
6884
const contextManager = serviceContainer.getContextManager();
6985
const commandManager = serviceContainer.getCommandManager();
7086

7187
// Migrate auth storage from old flat format to new label-based format
88+
activationTelemetry.setPhase("auth_migration");
7289
await migrateAuthStorage(serviceContainer);
7390

7491
// Clear and capture the startup mode before anything else.
92+
activationTelemetry.setPhase("startup_mode");
7593
const startupMode = await mementoManager.getAndClearStartupMode();
7694

95+
activationTelemetry.setPhase("deployment_read");
7796
const deployment = await secretsManager.getCurrentDeployment();
97+
const deploymentSessionAuth = deployment
98+
? await secretsManager.getSessionAuth(deployment.safeHostname)
99+
: undefined;
100+
activationTelemetry.setAuthState(
101+
deploymentSessionAuth ? "valid_token" : "none",
102+
);
78103

79104
// Shared handler for auth failures (used by interceptor + session manager)
80105
const handleAuthFailure = (): Promise<void> => {
@@ -99,6 +124,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
99124
};
100125

101126
// Create OAuth session manager - callback handles background refresh failures
127+
activationTelemetry.setPhase("oauth_session");
102128
const oauthSessionManager = OAuthSessionManager.create(
103129
deployment,
104130
serviceContainer,
@@ -109,10 +135,10 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
109135
// This client tracks the current login and will be used through the life of
110136
// the plugin to poll workspaces for the current login, as well as being used
111137
// in commands that operate on the current login.
138+
activationTelemetry.setPhase("client_setup");
112139
const client = CoderApi.create(
113140
deployment?.url || "",
114-
(await secretsManager.getSessionAuth(deployment?.safeHostname ?? ""))
115-
?.token,
141+
deploymentSessionAuth?.token,
116142
output,
117143
);
118144
ctx.subscriptions.push(client);
@@ -132,6 +158,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
132158

133159
const isAuthenticated = () => contextManager.get("coder.authenticated");
134160

161+
activationTelemetry.setPhase("tree_views");
135162
const myWorkspacesProvider = new WorkspaceProvider(
136163
WorkspaceQuery.Mine,
137164
client,
@@ -178,6 +205,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
178205
);
179206

180207
// Create deployment manager to centralize deployment state management
208+
activationTelemetry.setPhase("deployment_manager");
181209
const deploymentManager = DeploymentManager.create(
182210
serviceContainer,
183211
client,
@@ -188,6 +216,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
188216

189217
// Register globally available commands. Many of these have visibility
190218
// controlled by contexts, see `when` in the package.json.
219+
activationTelemetry.setPhase("commands");
191220
const commands = new Commands(serviceContainer, client, deploymentManager);
192221

193222
// Placeholder tree view for the coderTasks container when not authenticated.
@@ -363,6 +392,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
363392
// (this would require the user to uninstall the Coder extension and
364393
// reinstall after installing the remote SSH extension, which is annoying)
365394
if (remoteSshExtension && vscodeProposed.env.remoteAuthority) {
395+
activationTelemetry.setPhase("remote_setup");
366396
try {
367397
const details = await remote.setup(
368398
vscodeProposed.env.remoteAuthority,
@@ -372,11 +402,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
372402
if (details) {
373403
ctx.subscriptions.push(details);
374404

375-
await deploymentManager.setDeploymentIfValid({
405+
const deploymentSet = await deploymentManager.setDeploymentIfValid({
376406
safeHostname: details.safeHostname,
377407
url: details.url,
378408
token: details.token,
379409
});
410+
activationTelemetry.setAuthState(
411+
deploymentSet ? "valid_token" : "expired",
412+
);
380413

381414
// If a deep link stored a chat agent ID before the
382415
// remote-authority reload, open it now that the
@@ -391,7 +424,9 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
391424
} catch (ex) {
392425
if (ex instanceof CertificateError) {
393426
output.warn(ex.detail);
394-
await ex.showNotification("Failed to open workspace", { modal: true });
427+
await ex.showNotification("Failed to open workspace", {
428+
modal: true,
429+
});
395430
} else if (isAxiosError(ex)) {
396431
const msg = getErrorMessage(ex, "None");
397432
const detail = getErrorDetail(ex) || "None";
@@ -422,6 +457,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
422457
}
423458
// Always close remote session when we fail to open a workspace.
424459
await remote.closeRemote();
460+
activationTelemetry.complete();
425461
return;
426462
}
427463
}
@@ -431,10 +467,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
431467
if (deploymentManager.getCurrentDeployment()) {
432468
contextManager.set("coder.loaded", true);
433469
} else if (deployment) {
470+
activationTelemetry.setPhase("deployment_init");
434471
output.info(`Initializing deployment: ${deployment.url}`);
435-
deploymentManager
436-
.setDeploymentIfValid(deployment)
437-
// Failure is logged internally
472+
activationTelemetry
473+
.traceDeploymentInit(() =>
474+
deploymentManager.setDeploymentIfValid(deployment),
475+
)
438476
.then((success) => {
439477
if (success) {
440478
output.info("Deployment authenticated and set");
@@ -467,6 +505,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
467505
}
468506
}
469507
}
508+
activationTelemetry.complete();
470509
}
471510

472511
/**

0 commit comments

Comments
 (0)