Skip to content

Commit 78285e9

Browse files
committed
refactor: align telemetry outcomes with OpenTelemetry naming
Address review feedback and standardize span outcome vocabulary: - Rename failure -> error (markError, error.type, *ErrorCategory) to match the OTel span status and the error.type semantic convention. - Unify cancellation on "abort" across the telemetry layer; the VS Code cancellation mirrors (CancellationToken, ProgressResult) stay as-is. - OTLP export: map aborted spans to UNSET and backfill error.type on error spans from the exception type/code, falling back to OTel's _OTHER. - DevContainer casing, type-safe start/update button captions, and test cleanups (shared open defaults, multi-connected-agent coverage).
1 parent b27484a commit 78285e9

21 files changed

Lines changed: 230 additions & 131 deletions

src/commands.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import {
2525
} from "./instrumentation/diagnostics";
2626
import { WorkspaceOperationTelemetry } from "./instrumentation/workspace";
2727
import {
28-
type DevcontainerMode,
28+
type DevContainerMode,
2929
type WorkspaceOpenSource,
3030
WorkspaceOpenTelemetry,
3131
type WorkspaceOpenTrace,
32-
type WorkspacePickerFailureCategory,
32+
type WorkspacePickerErrorCategory,
3333
type WorkspacePickerResult,
3434
type WorkspacePickerSource,
3535
} from "./instrumentation/workspaceOpen";
@@ -102,7 +102,7 @@ type WorkspaceResolution =
102102
| { readonly status: "cancelled" }
103103
| {
104104
readonly status: "failed";
105-
readonly category: WorkspacePickerFailureCategory;
105+
readonly category: WorkspacePickerErrorCategory;
106106
};
107107

108108
type OpenWorkspaceResult =
@@ -281,7 +281,7 @@ export class Commands {
281281
): Promise<void> {
282282
const resolved = await this.resolveClientAndWorkspace(item);
283283
if (resolved.status === "cancelled") {
284-
telemetry.cancel("workspace_picker");
284+
telemetry.abort("workspace_picker");
285285
return;
286286
}
287287
if (resolved.status === "failed") {
@@ -304,7 +304,7 @@ export class Commands {
304304
},
305305
});
306306
if (input === undefined) {
307-
telemetry.cancel("input");
307+
telemetry.abort("input");
308308
return;
309309
}
310310
const seconds = Number(input.trim());
@@ -343,7 +343,7 @@ export class Commands {
343343

344344
if (!result.ok) {
345345
if (result.cancelled) {
346-
telemetry.cancel("progress");
346+
telemetry.abort("progress");
347347
return;
348348
}
349349
telemetry.fail();
@@ -391,7 +391,7 @@ export class Commands {
391391
): Promise<void> {
392392
const resolved = await this.resolveClientAndWorkspace(item);
393393
if (resolved.status === "cancelled") {
394-
telemetry.cancel("workspace_picker");
394+
telemetry.abort("workspace_picker");
395395
return;
396396
}
397397
if (resolved.status === "failed") {
@@ -403,7 +403,7 @@ export class Commands {
403403

404404
const outputUri = await this.promptSupportBundlePath();
405405
if (!outputUri) {
406-
telemetry.cancel("save_dialog");
406+
telemetry.abort("save_dialog");
407407
return;
408408
}
409409

@@ -441,7 +441,7 @@ export class Commands {
441441

442442
if (!result.ok) {
443443
if (result.cancelled) {
444-
telemetry.cancel("progress");
444+
telemetry.abort("progress");
445445
return;
446446
}
447447
telemetry.fail(
@@ -832,7 +832,7 @@ export class Commands {
832832
const agents = await this.extractAgentsWithFallback(item.workspace);
833833
const agent = await maybeAskAgent(agents);
834834
if (!agent) {
835-
telemetry.cancel("agent_picker", { workspace: item.workspace });
835+
telemetry.abort("agent_picker", { workspace: item.workspace });
836836
return false;
837837
}
838838
const selection = { workspace: item.workspace, agent };
@@ -911,7 +911,7 @@ export class Commands {
911911
} else {
912912
const pick = await this.pickWorkspace("workspace_open");
913913
if (pick.status === "cancelled") {
914-
telemetry.cancel("workspace_picker");
914+
telemetry.abort("workspace_picker");
915915
return false;
916916
}
917917
if (pick.status === "failed") {
@@ -924,7 +924,7 @@ export class Commands {
924924
const agents = await this.extractAgentsWithFallback(workspace);
925925
const agent = await maybeAskAgent(agents, agentName);
926926
if (!agent) {
927-
telemetry.cancel("agent_picker", { workspace });
927+
telemetry.abort("agent_picker", { workspace });
928928
return false;
929929
}
930930
const selection = { workspace, agent };
@@ -952,10 +952,10 @@ export class Commands {
952952
localWorkspaceFolder = "",
953953
localConfigFile = "",
954954
): Promise<void> {
955-
const mode: DevcontainerMode = localWorkspaceFolder
955+
const mode: DevContainerMode = localWorkspaceFolder
956956
? "dev_container"
957957
: "attached_container";
958-
await this.workspaceOpenTelemetry.traceDevcontainer(mode, () =>
958+
await this.workspaceOpenTelemetry.traceDevContainer(mode, () =>
959959
this.runOpenDevContainer(
960960
workspaceOwner,
961961
workspaceName,
@@ -1184,7 +1184,7 @@ export class Commands {
11841184

11851185
let lastWorkspaces: readonly Workspace[] = [];
11861186
let settled = false;
1187-
let fetchFailureCategory: WorkspacePickerFailureCategory | undefined;
1187+
let fetchErrorCategory: WorkspacePickerErrorCategory | undefined;
11881188
const disposables: vscode.Disposable[] = [];
11891189
disposables.push(
11901190
quickPick.onDidChangeValue((value) => {
@@ -1194,7 +1194,7 @@ export class Commands {
11941194
q: value,
11951195
})
11961196
.then((workspaces) => {
1197-
fetchFailureCategory = undefined;
1197+
fetchErrorCategory = undefined;
11981198
const filtered = filter
11991199
? workspaces.workspaces.filter(filter)
12001200
: workspaces.workspaces;
@@ -1224,7 +1224,7 @@ export class Commands {
12241224
}
12251225
})
12261226
.catch((ex) => {
1227-
fetchFailureCategory = "fetch_failed";
1227+
fetchErrorCategory = "fetch_failed";
12281228
this.logger.error("Failed to fetch workspaces", ex);
12291229
if (ex instanceof CertificateError) {
12301230
void ex.showNotification();
@@ -1256,10 +1256,10 @@ export class Commands {
12561256
};
12571257
disposables.push(
12581258
quickPick.onDidHide(() => {
1259-
if (fetchFailureCategory) {
1259+
if (fetchErrorCategory) {
12601260
finish({
12611261
status: "failed",
1262-
category: fetchFailureCategory,
1262+
category: fetchErrorCategory,
12631263
});
12641264
return;
12651265
}
@@ -1460,7 +1460,7 @@ function recordOpenResult(
14601460
result: OpenWorkspaceResult,
14611461
): boolean {
14621462
if (result.status === "cancelled") {
1463-
telemetry.cancel(result.stage, selection);
1463+
telemetry.abort(result.stage, selection);
14641464
return false;
14651465
}
14661466
telemetry.handoff(result.handoff);

src/core/cliCredentialManager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,8 @@ export class CliCredentialManager {
281281
binPath = await this.resolveKeyringBinary(url, configs, "keyringAuth");
282282
} catch (error) {
283283
this.logger.warn("Could not resolve keyring binary for delete:", error);
284-
span.setProperty("failure_category", "binary");
285-
span.markFailure();
284+
span.setProperty("error.type", "binary");
285+
span.markError();
286286
return;
287287
}
288288
if (!binPath) {
@@ -298,8 +298,8 @@ export class CliCredentialManager {
298298
throw error;
299299
}
300300
this.logger.warn("Failed to delete token via CLI:", error);
301-
span.setProperty("failure_category", "cli");
302-
span.markFailure();
301+
span.setProperty("error.type", "cli");
302+
span.markError();
303303
}
304304
}
305305

src/instrumentation/auth.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ export class AuthTelemetry {
117117
}
118118

119119
/**
120-
* Records `auth.login_prompted`. `auth_failed` marks the span as failure;
120+
* Records `auth.login_prompted`. `auth_failed` marks the span as error;
121121
* other non-success reasons mark it as aborted. The reason is copied to the
122-
* span's `reason` property on failure/abort only.
122+
* span's `reason` property on error/abort only.
123123
*/
124124
public traceLoginPrompt<T extends LoginPromptOutcome>(
125125
trigger: AuthLoginPromptTrigger,
@@ -139,11 +139,12 @@ export class AuthTelemetry {
139139
}
140140
}
141141

142-
/** `auth_failed` is a real failure; user/URL dismissals are intentional aborts. */
142+
/** `auth_failed` is a real error; user/URL dismissals are intentional aborts. */
143143
function recordReason(span: Span, reason: LoginPromptReason): void {
144144
span.setProperty("reason", reason);
145145
if (reason === "auth_failed") {
146-
span.markFailure();
146+
span.setProperty("error.type", reason);
147+
span.markError();
147148
} else {
148149
span.markAborted();
149150
}

src/instrumentation/credentials.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import type { WorkspaceConfiguration } from "vscode";
66
import type { TelemetryReporter } from "../telemetry/reporter";
77
import type { Span } from "../telemetry/span";
88

9-
export type CredentialFailureCategory = "aborted" | "binary" | "cli" | "file";
9+
export type CredentialErrorCategory = "aborted" | "binary" | "cli" | "file";
1010

1111
type CredentialEvent = "auth.credential.store" | "auth.credential.clear";
1212

1313
/**
1414
* Wraps credential store/clear in a span carrying `keyring_enabled`, the
15-
* `category` of storage involved, and a `failure_category` on failure. The
15+
* `category` of storage involved, and an `error.type` on failure. The
1616
* traced operation sets `category` on the span and reports failures by
1717
* throwing a categorized error (store) or recording on the span (clear, which
1818
* is best-effort). Aborts are recorded and re-thrown so callers still unwind.
@@ -47,10 +47,7 @@ export class CredentialTelemetry {
4747
try {
4848
await fn(span);
4949
} catch (error) {
50-
span.setProperty(
51-
"failure_category",
52-
categorizeCredentialError(error),
53-
);
50+
span.setProperty("error.type", categorizeCredentialError(error));
5451
if (isAbortError(error)) {
5552
span.markAborted();
5653
aborted = error;
@@ -70,7 +67,7 @@ export class CredentialTelemetry {
7067
}
7168
}
7269

73-
function categorizeCredentialError(error: unknown): CredentialFailureCategory {
70+
function categorizeCredentialError(error: unknown): CredentialErrorCategory {
7471
if (isAbortError(error)) {
7572
return "aborted";
7673
}

src/instrumentation/diagnostics.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
import { recordCancelled, recordFailure } from "./outcomes";
1+
import { recordAborted, recordError } from "./outcomes";
22

33
import type { SpeedtestResult } from "@repo/shared";
44

55
import type { TelemetryReporter } from "../telemetry/reporter";
66
import type { Span } from "../telemetry/span";
77

8-
import type { WorkspacePickerFailureCategory } from "./workspaceOpen";
8+
import type { WorkspacePickerErrorCategory } from "./workspaceOpen";
99

1010
export type DiagnosticCommand =
1111
| "speed_test"
1212
| "support_bundle"
1313
| "export_telemetry";
14-
export type DiagnosticFailureCategory =
15-
| WorkspacePickerFailureCategory
14+
export type DiagnosticErrorCategory =
15+
| WorkspacePickerErrorCategory
1616
| "parse_error"
1717
| "unsupported_cli"
1818
| "error";
19-
export type DiagnosticCancelStage =
19+
export type DiagnosticAbortStage =
2020
| "workspace_picker"
2121
| "input"
2222
| "prompt"
2323
| "save_dialog"
2424
| "progress";
2525

2626
export interface DiagnosticTrace {
27-
cancel(stage: DiagnosticCancelStage): void;
28-
fail(category?: DiagnosticFailureCategory): void;
27+
abort(stage: DiagnosticAbortStage): void;
28+
fail(category?: DiagnosticErrorCategory): void;
2929
setRequestedDuration(seconds: number): void;
3030
succeedSpeedtest(result: SpeedtestResult): void;
3131
succeedExport(format: string, eventCount: number): void;
@@ -50,12 +50,12 @@ export class DiagnosticTelemetry {
5050
class SpanDiagnosticTrace implements DiagnosticTrace {
5151
public constructor(private readonly span: Span) {}
5252

53-
public cancel(stage: DiagnosticCancelStage): void {
54-
recordCancelled(this.span, stage);
53+
public abort(stage: DiagnosticAbortStage): void {
54+
recordAborted(this.span, stage);
5555
}
5656

57-
public fail(category: DiagnosticFailureCategory = "error"): void {
58-
recordFailure(this.span, category);
57+
public fail(category: DiagnosticErrorCategory = "error"): void {
58+
recordError(this.span, category);
5959
}
6060

6161
public setRequestedDuration(seconds: number): void {

src/instrumentation/outcomes.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ import { isAbortError } from "../error/errorUtils";
22

33
import type { Span } from "../telemetry/span";
44

5-
export type AbortableFailureCategory = "aborted" | "error";
5+
export type AbortableErrorCategory = "aborted" | "error";
66

7-
/** Records a categorized failure without attaching raw error details. */
8-
export function recordFailure(span: Span, category: string): void {
9-
span.setProperty("failure_category", category);
10-
span.markFailure();
7+
/** Records a categorized error without attaching the raw error details. */
8+
export function recordError(span: Span, category: string): void {
9+
span.setProperty("error.type", category);
10+
span.markError();
1111
}
1212

1313
/** Records the stage at which the user backed out and aborts the span. */
14-
export function recordCancelled(span: Span, stage: string): void {
15-
span.setProperty("cancel_stage", stage);
14+
export function recordAborted(span: Span, stage: string): void {
15+
span.setProperty("abort_stage", stage);
1616
span.markAborted();
1717
}
1818

19-
export function categorizeAbortableFailure(
19+
export function categorizeAbortableError(
2020
error: unknown,
21-
): AbortableFailureCategory {
21+
): AbortableErrorCategory {
2222
return isAbortError(error) ? "aborted" : "error";
2323
}

0 commit comments

Comments
 (0)