Skip to content

Commit ae1920a

Browse files
authored
refactor(telemetry): OTel-format ids and explicit session id (#935)
Cleans up the local JSONL schema: `session_id` no longer leaks `vscode.env.sessionId`'s UUID+timestamp shape, and `event_id` / `trace_id` use OTel lowercase hex instead of UUIDv4. - New `src/telemetry/ids.ts`: `newTraceId` (32 hex), `newSpanId` (16 hex), `newSessionId` (32 hex) - `ServiceContainer` generates one session id and threads it to both sink filename and event payload - Tests updated for the new id format Follow-up to #934
1 parent d5de1ce commit ae1920a

7 files changed

Lines changed: 55 additions & 24 deletions

File tree

src/core/container.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ 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 { extractExtensionVersion } from "../telemetry/event";
6+
import { buildSession, extractExtensionVersion } from "../telemetry/event";
7+
import { newSessionId } from "../telemetry/ids";
78
import { TelemetryService } from "../telemetry/service";
89
import { LocalJsonlSink } from "../telemetry/sinks/localJsonlSink";
910
import { SpeedtestPanelFactory } from "../webviews/speedtest/speedtestPanelFactory";
@@ -94,15 +95,21 @@ export class ServiceContainer implements vscode.Disposable {
9495
context.extensionUri,
9596
this.logger,
9697
);
98+
99+
const sessionId = newSessionId();
97100
const localJsonlSink = LocalJsonlSink.start(
98101
{
99102
baseDir: this.pathResolver.getTelemetryPath(),
100-
sessionId: vscode.env.sessionId,
103+
sessionId,
101104
},
102105
this.logger,
103106
);
104-
this.telemetryService = new TelemetryService(
107+
const session = buildSession(
105108
extractExtensionVersion(context.extension.packageJSON),
109+
sessionId,
110+
);
111+
this.telemetryService = new TelemetryService(
112+
session,
106113
[localJsonlSink],
107114
this.logger,
108115
);

src/telemetry/event.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,14 @@ export interface TelemetrySink {
6969
}
7070

7171
/** Build session attributes from the extension version and ambient host data. */
72-
export function buildSession(extensionVersion: string): SessionContext {
72+
export function buildSession(
73+
extensionVersion: string,
74+
sessionId: string,
75+
): SessionContext {
7376
return {
7477
extensionVersion,
7578
machineId: vscode.env.machineId,
76-
sessionId: vscode.env.sessionId,
79+
sessionId,
7780
osType: detectOsType(),
7881
osVersion: os.release(),
7982
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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
} from "../settings/telemetry";
99

1010
import {
11-
buildSession,
1211
buildErrorBlock,
1312
type CallerMeasurements,
1413
type CallerProperties,
@@ -17,6 +16,7 @@ import {
1716
type TelemetryLevel,
1817
type TelemetrySink,
1918
} from "./event";
19+
import { newSpanId, newTraceId } from "./ids";
2020
import { NOOP_SPAN, type Span } from "./span";
2121

2222
const LEVEL_ORDER: Readonly<Record<TelemetryLevel, number>> = {
@@ -52,11 +52,11 @@ export class TelemetryService implements vscode.Disposable {
5252
readonly #configWatcher: vscode.Disposable;
5353

5454
public constructor(
55-
extensionVersion: string,
55+
session: SessionContext,
5656
private readonly sinks: readonly TelemetrySink[],
5757
private readonly logger: Logger,
5858
) {
59-
this.#session = buildSession(extensionVersion);
59+
this.#session = session;
6060
this.#level = readLevel();
6161
this.#configWatcher = watchConfigurationChanges(
6262
[{ setting: TELEMETRY_LEVEL_SETTING, getValue: readLevel }],
@@ -86,7 +86,7 @@ export class TelemetryService implements vscode.Disposable {
8686
if (this.#level === "off") {
8787
return;
8888
}
89-
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements);
89+
this.#safeEmit(newSpanId(), eventName, properties, measurements);
9090
}
9191

9292
public logError(
@@ -98,7 +98,7 @@ export class TelemetryService implements vscode.Disposable {
9898
if (this.#level === "off") {
9999
return;
100100
}
101-
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements, {
101+
this.#safeEmit(newSpanId(), eventName, properties, measurements, {
102102
error,
103103
});
104104
}
@@ -118,7 +118,7 @@ export class TelemetryService implements vscode.Disposable {
118118
return fn(NOOP_SPAN);
119119
}
120120
return this.#startSpan(eventName, fn, properties, measurements, {
121-
traceId: crypto.randomUUID(),
121+
traceId: newTraceId(),
122122
traceLevel: this.#level,
123123
});
124124
}
@@ -140,7 +140,7 @@ export class TelemetryService implements vscode.Disposable {
140140
measurements: Record<string, number>,
141141
spanOpts: SpanOptions,
142142
): Promise<T> {
143-
const eventId = crypto.randomUUID();
143+
const eventId = newSpanId();
144144
const { traceId, traceLevel } = spanOpts;
145145
const span: Span = {
146146
traceId,

test/unit/core/commandManager.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
CommandManager,
99
type CoderCommandId,
1010
} from "@/core/commandManager";
11+
import { buildSession } from "@/telemetry/event";
1112
import { TelemetryService } from "@/telemetry/service";
1213

1314
import { TestSink } from "../../mocks/telemetry";
@@ -26,7 +27,7 @@ function makeHarness(): Harness {
2627
config.set("coder.telemetry.level", "local");
2728
const sink = new TestSink();
2829
const telemetry = new TelemetryService(
29-
"1.2.3-test",
30+
buildSession("1.2.3-test", "test-session"),
3031
[sink],
3132
createMockLogger(),
3233
);

test/unit/telemetry/event.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import {
77
} from "@/telemetry/event";
88

99
describe("buildSession", () => {
10-
it("populates session-stable fields from the version, vscode env, and host", () => {
11-
const session = buildSession("1.2.3-test");
10+
it("populates session-stable fields from the version, sessionId, vscode env, and host", () => {
11+
const session = buildSession("1.2.3-test", "session-abc");
1212

1313
expect(session).toMatchObject({
1414
extensionVersion: "1.2.3-test",
1515
machineId: "test-machine-id",
16-
sessionId: "test-session-id",
16+
sessionId: "session-abc",
1717
platformName: "Visual Studio Code",
1818
platformVersion: "1.106.0-test",
1919
hostArch: process.arch,

test/unit/telemetry/service.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22

3+
import { buildSession, type TelemetrySink } from "@/telemetry/event";
34
import { TelemetryService } from "@/telemetry/service";
45

56
import { TestSink } from "../../mocks/telemetry";
@@ -8,9 +9,10 @@ import {
89
MockConfigurationProvider,
910
} from "../../mocks/testHelpers";
1011

11-
import type { TelemetrySink } from "@/telemetry/event";
12-
1312
const TEST_VERSION = "1.2.3-test";
13+
const TEST_SESSION_ID = "test-session";
14+
15+
const testSession = () => buildSession(TEST_VERSION, TEST_SESSION_ID);
1416

1517
interface Harness {
1618
service: TelemetryService;
@@ -23,7 +25,7 @@ function makeHarness(level: "off" | "local" = "local"): Harness {
2325
config.set("coder.telemetry.level", level);
2426
const sink = new TestSink();
2527
const service = new TelemetryService(
26-
TEST_VERSION,
28+
testSession(),
2729
[sink],
2830
createMockLogger(),
2931
);
@@ -32,7 +34,7 @@ function makeHarness(level: "off" | "local" = "local"): Harness {
3234

3335
function makeService(sinks: TelemetrySink[]): TelemetryService {
3436
new MockConfigurationProvider().set("coder.telemetry.level", "local");
35-
return new TelemetryService(TEST_VERSION, sinks, createMockLogger());
37+
return new TelemetryService(testSession(), sinks, createMockLogger());
3638
}
3739

3840
describe("TelemetryService", () => {
@@ -57,13 +59,11 @@ describe("TelemetryService", () => {
5759
context: {
5860
extensionVersion: "1.2.3-test",
5961
machineId: "test-machine-id",
60-
sessionId: "test-session-id",
62+
sessionId: TEST_SESSION_ID,
6163
deploymentUrl: "",
6264
},
6365
});
64-
expect(event.eventId).toMatch(
65-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
66-
);
66+
expect(event.eventId).toMatch(/^[0-9a-f]{16}$/);
6767
expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
6868
});
6969

0 commit comments

Comments
 (0)