Skip to content

Commit 5908f42

Browse files
committed
refactor(telemetry): fix convention violations from instrumentation audit
- rename command.invoked commandId -> command_id and CliCacheSource values to snake_case (file_path, not_found) - lowercase ConnectionState values on connection.state_transitioned; rename the caller-set result key on connection.reconnect_resolved to outcome so result stays framework-managed - thrown aborts mark spans aborted instead of error.type="aborted" (workspace.open, dev container open, credential store/clear) - type the diagnostic export format as ExportFormat, wrap auth.session_lookup in AuthTelemetry, resolve _seconds measurement suffixes to an OTLP unit
1 parent e1eb693 commit 5908f42

18 files changed

Lines changed: 99 additions & 73 deletions

src/core/cliManager.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import type { CliCredentialManager } from "./cliCredentialManager";
4343
import type { PathResolver } from "./pathResolver";
4444

4545
type ResolvedBinary =
46-
| { binPath: string; stat: Stats; source: "file-path" | "directory" }
47-
| { binPath: string; source: "not-found" };
46+
| { binPath: string; stat: Stats; source: "file_path" | "directory" }
47+
| { binPath: string; source: "not_found" };
4848

4949
type CliVerifyResult =
5050
| { kind: "verified" }
@@ -77,7 +77,7 @@ export class CliManager {
7777
public async locateBinary(url: string): Promise<string> {
7878
const safeHostname = toSafeHost(url);
7979
const resolved = await this.resolveBinaryPath(safeHostname);
80-
if (resolved.source === "not-found") {
80+
if (resolved.source === "not_found") {
8181
throw new Error(`No CLI binary found at ${resolved.binPath}`);
8282
}
8383
return resolved.binPath;
@@ -86,9 +86,9 @@ export class CliManager {
8686
/**
8787
* Resolve the CLI binary path from the configured cache path.
8888
*
89-
* Returns "file-path" when the cache path is an existing file (checked for
89+
* Returns "file_path" when the cache path is an existing file (checked for
9090
* version match and updated if needed), "directory" when a binary was found
91-
* inside the directory, or "not-found" with the platform-specific path for
91+
* inside the directory, or "not_found" with the platform-specific path for
9292
* the caller to download into.
9393
*/
9494
private async resolveBinaryPath(
@@ -98,14 +98,14 @@ export class CliManager {
9898
const cacheStat = await cliUtils.stat(cachePath);
9999

100100
if (cacheStat?.isFile()) {
101-
return { binPath: cachePath, stat: cacheStat, source: "file-path" };
101+
return { binPath: cachePath, stat: cacheStat, source: "file_path" };
102102
}
103103

104104
const fullNamePath = path.join(cachePath, cliUtils.fullName());
105105

106106
// Path does not exist yet; return the platform-specific path to download.
107107
if (!cacheStat) {
108-
return { binPath: fullNamePath, source: "not-found" };
108+
return { binPath: fullNamePath, source: "not_found" };
109109
}
110110

111111
// Directory exists; check platform-specific name, then simple name.
@@ -120,7 +120,7 @@ export class CliManager {
120120
return { binPath: simpleNamePath, stat: simpleStat, source: "directory" };
121121
}
122122

123-
return { binPath: fullNamePath, source: "not-found" };
123+
return { binPath: fullNamePath, source: "not_found" };
124124
}
125125

126126
/**
@@ -240,7 +240,7 @@ export class CliManager {
240240
);
241241
}
242242

243-
if (resolved.source === "not-found") {
243+
if (resolved.source === "not_found") {
244244
this.output.info("No existing binary found, starting download");
245245
return {
246246
buildInfo,
@@ -442,7 +442,7 @@ export class CliManager {
442442
downloadBinPath: string,
443443
): Promise<string> {
444444
if (
445-
resolved.source === "file-path" &&
445+
resolved.source === "file_path" &&
446446
downloadBinPath !== resolved.binPath
447447
) {
448448
this.output.info(

src/core/commandManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class CommandManager implements vscode.Disposable {
6262
}
6363

6464
const invoke = handler as (...args: unknown[]) => unknown;
65-
const properties = { commandId: id };
65+
const properties = { command_id: id };
6666
const wrapped = (...args: unknown[]): Thenable<unknown> =>
6767
this.telemetry.trace(
6868
COMMAND_INVOKED_EVENT,

src/instrumentation/CONVENTIONS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ Coder's own pipeline, so a bare `cache_source` can't collide with a future OTel
116116
export as record attributes, and a query can bucket the raw number at read
117117
time. `result` and `durationMs` are framework-managed and cannot be set.
118118
- **Units.** There is no unit field at emit time, so put the unit in the
119-
measurement key as a `_ms` / `_mbits` suffix, the same way for every event.
119+
measurement key as a `_ms` / `_seconds` / `_mbits` suffix, the same way for
120+
every event.
120121
The OTLP exporter then resolves it per signal: for metric events
121122
(`http.requests`, `ssh.network.sampled`) it moves the suffix into the OTLP
122123
`unit` field and drops it from the metric name (`latency_ms` exports as metric

src/instrumentation/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ export class AuthTelemetry {
8484
});
8585
}
8686

87+
/** Wraps the secret-storage session read that seeds a remote connection. */
88+
public traceSessionLookup<T>(fn: () => Promise<T>): Promise<T> {
89+
return this.telemetry.trace("auth.session_lookup", fn);
90+
}
91+
8792
public traceTokenRefresh<T>(
8893
trigger: AuthTokenRefreshTrigger,
8994
fn: () => Promise<T>,

src/instrumentation/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { CallerPropertyValue } from "../telemetry/event";
55
import type { TelemetryService } from "../telemetry/service";
66
import type { Span } from "../telemetry/span";
77

8-
export type CliCacheSource = "file-path" | "directory" | "not-found";
8+
export type CliCacheSource = "file_path" | "directory" | "not_found";
99
export type CliDownloadReason = "missing" | "version_mismatch" | "unreadable";
1010
export type CliDownloadAction = "download" | "fallback" | "blocked";
1111
export type CliCredentialSource = "session_token" | "empty_token";

src/instrumentation/credentials.ts

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

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

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

@@ -47,12 +47,12 @@ export class CredentialTelemetry {
4747
try {
4848
await fn(span);
4949
} catch (error) {
50-
span.setProperty("error.type", categorizeCredentialError(error));
5150
if (isAbortError(error)) {
5251
span.markAborted();
5352
aborted = error;
5453
return;
5554
}
55+
span.setProperty("error.type", categorizeCredentialError(error));
5656
throw error;
5757
}
5858
},
@@ -68,9 +68,6 @@ export class CredentialTelemetry {
6868
}
6969

7070
function categorizeCredentialError(error: unknown): CredentialErrorCategory {
71-
if (isAbortError(error)) {
72-
return "aborted";
73-
}
7471
if (error instanceof CredentialFileError) {
7572
return "file";
7673
}

src/instrumentation/diagnostics.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { recordAborted, recordError } from "./outcomes";
22

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

5+
import type { ExportFormat } from "../telemetry/export/writers/types";
56
import type { TelemetryReporter } from "../telemetry/reporter";
67
import type { Span } from "../telemetry/span";
78

@@ -28,7 +29,7 @@ export interface DiagnosticTrace {
2829
error(category?: DiagnosticErrorCategory): void;
2930
setRequestedDuration(seconds: number): void;
3031
succeedSpeedtest(result: SpeedtestResult): void;
31-
succeedExport(format: string, eventCount: number): void;
32+
succeedExport(format: ExportFormat, eventCount: number): void;
3233
}
3334

3435
/** Emits `command.diagnostic.completed` around each diagnostic command. */
@@ -70,7 +71,7 @@ class SpanDiagnosticTrace implements DiagnosticTrace {
7071
);
7172
}
7273

73-
public succeedExport(format: string, eventCount: number): void {
74+
public succeedExport(format: ExportFormat, eventCount: number): void {
7475
this.span.setProperty("format", format);
7576
this.span.setMeasurement("event.count", eventCount);
7677
}

src/instrumentation/outcomes.ts

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

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

5-
export type AbortableErrorCategory = "aborted" | "error";
6-
75
/** Records a categorized error without attaching the raw error details. */
86
export function recordError(span: Span, category: string): void {
97
span.setProperty("error.type", category);
@@ -16,8 +14,11 @@ export function recordAborted(span: Span, stage: string): void {
1614
span.markAborted();
1715
}
1816

19-
export function categorizeAbortableError(
20-
error: unknown,
21-
): AbortableErrorCategory {
22-
return isAbortError(error) ? "aborted" : "error";
17+
/** Marks a thrown abort as `aborted`; records anything else as a categorized `"error"`. */
18+
export function recordAbortableError(span: Span, error: unknown): void {
19+
if (isAbortError(error)) {
20+
span.markAborted();
21+
} else {
22+
recordError(span, "error");
23+
}
2324
}

src/instrumentation/websocket.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ export type ConnectionDropCause =
2929
| "error";
3030

3131
type ReconnectOutcome =
32-
| { readonly result: "success" }
32+
| { readonly outcome: "success" }
3333
| {
34-
readonly result: "error";
34+
readonly outcome: "error";
3535
readonly terminationReason: ConnectionStateReason;
3636
};
3737

38+
/** `ConnectionState` lowercased into the OTel snake_case attribute value. */
39+
type ConnectionStateValue = Lowercase<`${ConnectionState}`>;
40+
41+
function stateValue(state: ConnectionState): ConnectionStateValue {
42+
return state.toLowerCase() as ConnectionStateValue;
43+
}
44+
3845
interface ReconnectCycle {
3946
readonly startMs: number;
4047
readonly reason: ConnectionStateReason;
@@ -64,8 +71,8 @@ export class WebSocketTelemetry {
6471
reason: ConnectionStateReason,
6572
): void {
6673
this.#telemetry.log("connection.state_transitioned", {
67-
from,
68-
to,
74+
from: stateValue(from),
75+
to: stateValue(to),
6976
reason,
7077
});
7178
}
@@ -88,7 +95,7 @@ export class WebSocketTelemetry {
8895
{ route: normalizeRoute(route) },
8996
{ connect_duration_ms: now - start },
9097
);
91-
this.#finishReconnect({ result: "success" });
98+
this.#finishReconnect({ outcome: "success" });
9299
}
93100

94101
public dropped(
@@ -144,7 +151,7 @@ export class WebSocketTelemetry {
144151
/** Drop and end the reconnect cycle as a failure. */
145152
public terminated(reason: ConnectionStateReason, options: DropOptions): void {
146153
this.dropped(options.cause, options.code, options.error);
147-
this.#finishReconnect({ result: "error", terminationReason: reason });
154+
this.#finishReconnect({ outcome: "error", terminationReason: reason });
148155
}
149156

150157
/** Drop and (re)open a reconnect cycle. */
@@ -168,11 +175,11 @@ export class WebSocketTelemetry {
168175
}
169176
this.#reconnectCycle = undefined;
170177

171-
const properties: Record<string, string> = {
172-
result: outcome.result,
178+
const properties: CallerProperties = {
179+
outcome: outcome.outcome,
173180
reason: cycle.reason,
174181
};
175-
if (outcome.result === "error") {
182+
if (outcome.outcome === "error") {
176183
properties.termination_reason = outcome.terminationReason;
177184
}
178185
this.#telemetry.log("connection.reconnect_resolved", properties, {

src/instrumentation/workspaceOpen.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { extractAgents } from "../api/api-helper";
22

3-
import {
4-
type AbortableErrorCategory,
5-
categorizeAbortableError,
6-
recordAborted,
7-
recordError,
8-
} from "./outcomes";
3+
import { recordAbortableError, recordAborted, recordError } from "./outcomes";
94

105
import type {
116
Workspace,
@@ -25,9 +20,7 @@ export type WorkspaceOpenSource =
2520

2621
export type WorkspacePickerSource = "workspace_open" | "diagnostic";
2722
export type WorkspacePickerErrorCategory = "fetch_error";
28-
export type WorkspaceOpenErrorCategory =
29-
| WorkspacePickerErrorCategory
30-
| AbortableErrorCategory;
23+
export type WorkspaceOpenErrorCategory = WorkspacePickerErrorCategory | "error";
3124
export type WorkspacePickerResult =
3225
| { readonly status: "selected"; readonly workspace: Workspace }
3326
| { readonly status: "cancelled" }
@@ -114,8 +107,9 @@ export class WorkspaceOpenTelemetry {
114107
}
115108

116109
/**
117-
* Runs `fn` inside the span, recording a thrown error as a categorized
118-
* error without its raw details, then rethrows outside the span.
110+
* Runs `fn` inside the span, recording a thrown abort as `aborted` and any
111+
* other error as a categorized error without its raw details, then rethrows
112+
* outside the span.
119113
*/
120114
private async traceRethrowing<T>(
121115
eventName: string,
@@ -131,7 +125,7 @@ export class WorkspaceOpenTelemetry {
131125
return await fn(span);
132126
} catch (error) {
133127
thrown = { error };
134-
recordError(span, categorizeAbortableError(error));
128+
recordAbortableError(span, error);
135129
return fallback;
136130
}
137131
},

0 commit comments

Comments
 (0)