Skip to content

Commit 6601664

Browse files
committed
refactor(telemetry): OTel-format ids and explicit session id
The local JSONL output exposed two schema rough edges. First, session_id was vscode.env.sessionId verbatim, which is a UUID concatenated with a ms timestamp ("0f465473-...-bed92cb4ed3a1777982179036"), looking like a malformed UUID. Second, event_id and trace_id were UUIDv4 with hyphens, not the lowercase-hex form OTel uses, so a future exporter would need a translation layer for no real reason. - Add src/telemetry/ids.ts with newTraceId (16 bytes / 32 hex), newSpanId (8 bytes / 16 hex), and newSessionId (16 bytes / 32 hex). Names and widths match OTel. - buildSession takes sessionId as a parameter instead of reading vscode.env.sessionId, decoupling our schema from VS Code's quirks. - TelemetryService accepts sessionId in its constructor and forwards it to buildSession. - ServiceContainer generates one sessionId via newSessionId() and threads it to both LocalJsonlSink (filename slug) and TelemetryService (event payload), so the on-disk filename and the session_id field always match. - service.ts: replace crypto.randomUUID() with newSpanId / newTraceId at every event emission. - Tests updated for the new sessionId parameter and the new id format regex (/^[0-9a-f]{16}$/ for event_id, sessionId is now an explicit test fixture). trace_id stays on every event (including single-event "traces"). You cannot know at emit time whether a phase child will follow, and a consistent schema is more valuable to consumers than 36 bytes per event.
1 parent 0e6d1a1 commit 6601664

7 files changed

Lines changed: 57 additions & 18 deletions

File tree

src/core/container.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from "vscode";
33
import { CoderApi } from "../api/coderApi";
44
import { LoginCoordinator } from "../login/loginCoordinator";
55
import { OAuthCallback } from "../oauth/oauthCallback";
6+
import { newSessionId } from "../telemetry/ids";
67
import { TelemetryService } from "../telemetry/service";
78
import { LocalJsonlSink } from "../telemetry/sinks/localJsonlSink";
89
import { SpeedtestPanelFactory } from "../webviews/speedtest/speedtestPanelFactory";
@@ -93,15 +94,18 @@ export class ServiceContainer implements vscode.Disposable {
9394
context.extensionUri,
9495
this.logger,
9596
);
97+
// Shared by the sink (filename) and the service (event payload).
98+
const sessionId = newSessionId();
9699
const localJsonlSink = LocalJsonlSink.start(
97100
{
98101
baseDir: this.pathResolver.getTelemetryPath(),
99-
sessionId: vscode.env.sessionId,
102+
sessionId,
100103
},
101104
this.logger,
102105
);
103106
this.telemetryService = new TelemetryService(
104107
context,
108+
sessionId,
105109
[localJsonlSink],
106110
this.logger,
107111
);

src/telemetry/event.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ export interface TelemetrySink {
6868
dispose(): Promise<void>;
6969
}
7070

71-
/** Build session attributes. `extensionVersion` falls back to `"unknown"`. */
72-
export function buildSession(ctx: vscode.ExtensionContext): SessionContext {
71+
/** Build session attributes. `extensionVersion` falls back to `"unknown"`.
72+
* `sessionId` is caller-supplied so payload and sink filename agree. */
73+
export function buildSession(
74+
ctx: vscode.ExtensionContext,
75+
sessionId: string,
76+
): SessionContext {
7377
// "unknown" only for malformed package.json or test fixtures missing `version`.
7478
const packageJson = ctx.extension.packageJSON as { version?: unknown };
7579
const extensionVersion =
@@ -78,7 +82,7 @@ export function buildSession(ctx: vscode.ExtensionContext): SessionContext {
7882
return {
7983
extensionVersion,
8084
machineId: vscode.env.machineId,
81-
sessionId: vscode.env.sessionId,
85+
sessionId,
8286
osType: detectOsType(),
8387
osVersion: os.release(),
8488
hostArch: process.arch,

src/telemetry/ids.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { randomBytes } from "node:crypto";
2+
3+
// OTel-format ids: lowercase hex, no separators. Keeps a future OTel
4+
// exporter a 1:1 mapping.
5+
6+
/** OTel `trace_id`: 16 bytes / 32 hex. */
7+
export function newTraceId(): string {
8+
return randomBytes(16).toString("hex");
9+
}
10+
11+
/** OTel `span_id` (used as `event_id`): 8 bytes / 16 hex. */
12+
export function newSpanId(): string {
13+
return randomBytes(8).toString("hex");
14+
}
15+
16+
/** Our own session id (16 bytes / 32 hex). Avoids `vscode.env.sessionId`,
17+
* which is a UUID concatenated with a timestamp. */
18+
export function newSessionId(): string {
19+
return randomBytes(16).toString("hex");
20+
}

src/telemetry/service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type TelemetryLevel,
1818
type TelemetrySink,
1919
} from "./event";
20+
import { newSpanId, newTraceId } from "./ids";
2021
import { NOOP_SPAN, type Span } from "./span";
2122

2223
const LEVEL_ORDER: Readonly<Record<TelemetryLevel, number>> = {
@@ -53,10 +54,11 @@ export class TelemetryService implements vscode.Disposable {
5354

5455
public constructor(
5556
ctx: vscode.ExtensionContext,
57+
sessionId: string,
5658
private readonly sinks: readonly TelemetrySink[],
5759
private readonly logger: Logger,
5860
) {
59-
this.#session = buildSession(ctx);
61+
this.#session = buildSession(ctx, sessionId);
6062
this.#level = readLevel();
6163
this.#configWatcher = watchConfigurationChanges(
6264
[{ setting: TELEMETRY_LEVEL_SETTING, getValue: readLevel }],
@@ -86,7 +88,7 @@ export class TelemetryService implements vscode.Disposable {
8688
if (this.#level === "off") {
8789
return;
8890
}
89-
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements);
91+
this.#safeEmit(newSpanId(), eventName, properties, measurements);
9092
}
9193

9294
public logError(
@@ -98,7 +100,7 @@ export class TelemetryService implements vscode.Disposable {
98100
if (this.#level === "off") {
99101
return;
100102
}
101-
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements, {
103+
this.#safeEmit(newSpanId(), eventName, properties, measurements, {
102104
error,
103105
});
104106
}
@@ -118,7 +120,7 @@ export class TelemetryService implements vscode.Disposable {
118120
return fn(NOOP_SPAN);
119121
}
120122
return this.#startSpan(eventName, fn, properties, measurements, {
121-
traceId: crypto.randomUUID(),
123+
traceId: newTraceId(),
122124
traceLevel: this.#level,
123125
});
124126
}
@@ -140,7 +142,7 @@ export class TelemetryService implements vscode.Disposable {
140142
measurements: Record<string, number>,
141143
spanOpts: SpanOptions,
142144
): Promise<T> {
143-
const eventId = crypto.randomUUID();
145+
const eventId = newSpanId();
144146
const { traceId, traceLevel } = spanOpts;
145147
const span: Span = {
146148
traceId,

test/unit/core/commandManager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function makeHarness(): Harness {
3535
const sink = new TestSink();
3636
const telemetry = new TelemetryService(
3737
fakeContext(),
38+
"test-session",
3839
[sink],
3940
createMockLogger(),
4041
);

test/unit/telemetry/event.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ function fakeContext(version: unknown = "1.2.3-test"): vscode.ExtensionContext {
1212

1313
describe("buildSession", () => {
1414
it("populates session-stable fields from the extension context, vscode env, and host", () => {
15-
const session = buildSession(fakeContext());
15+
const session = buildSession(fakeContext(), "session-abc");
1616

1717
expect(session).toMatchObject({
1818
extensionVersion: "1.2.3-test",
1919
machineId: "test-machine-id",
20-
sessionId: "test-session-id",
20+
sessionId: "session-abc",
2121
platformName: "Visual Studio Code",
2222
platformVersion: "1.106.0-test",
2323
hostArch: process.arch,
@@ -32,8 +32,10 @@ describe("buildSession", () => {
3232
const noVersion = {
3333
extension: { packageJSON: {} },
3434
} as unknown as vscode.ExtensionContext;
35-
expect(buildSession(noVersion).extensionVersion).toBe("unknown");
36-
expect(buildSession(fakeContext(123)).extensionVersion).toBe("unknown");
35+
expect(buildSession(noVersion, "s").extensionVersion).toBe("unknown");
36+
expect(buildSession(fakeContext(123), "s").extensionVersion).toBe(
37+
"unknown",
38+
);
3739
});
3840
});
3941

test/unit/telemetry/service.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ interface Harness {
2424
config: MockConfigurationProvider;
2525
}
2626

27+
const TEST_SESSION_ID = "test-session";
28+
2729
function makeHarness(level: "off" | "local" = "local"): Harness {
2830
const config = new MockConfigurationProvider();
2931
config.set("coder.telemetry.level", level);
3032
const sink = new TestSink();
3133
const service = new TelemetryService(
3234
fakeContext(),
35+
TEST_SESSION_ID,
3336
[sink],
3437
createMockLogger(),
3538
);
@@ -38,7 +41,12 @@ function makeHarness(level: "off" | "local" = "local"): Harness {
3841

3942
function makeService(sinks: TelemetrySink[]): TelemetryService {
4043
new MockConfigurationProvider().set("coder.telemetry.level", "local");
41-
return new TelemetryService(fakeContext(), sinks, createMockLogger());
44+
return new TelemetryService(
45+
fakeContext(),
46+
TEST_SESSION_ID,
47+
sinks,
48+
createMockLogger(),
49+
);
4250
}
4351

4452
describe("TelemetryService", () => {
@@ -63,13 +71,11 @@ describe("TelemetryService", () => {
6371
context: {
6472
extensionVersion: "1.2.3-test",
6573
machineId: "test-machine-id",
66-
sessionId: "test-session-id",
74+
sessionId: TEST_SESSION_ID,
6775
deploymentUrl: "",
6876
},
6977
});
70-
expect(event.eventId).toMatch(
71-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
72-
);
78+
expect(event.eventId).toMatch(/^[0-9a-f]{16}$/);
7379
expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
7480
});
7581

0 commit comments

Comments
 (0)