Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/core/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as vscode from "vscode";
import { CoderApi } from "../api/coderApi";
import { LoginCoordinator } from "../login/loginCoordinator";
import { OAuthCallback } from "../oauth/oauthCallback";
import { extractExtensionVersion } from "../telemetry/event";
import { buildSession, extractExtensionVersion } from "../telemetry/event";
import { newSessionId } from "../telemetry/ids";
import { TelemetryService } from "../telemetry/service";
import { LocalJsonlSink } from "../telemetry/sinks/localJsonlSink";
import { SpeedtestPanelFactory } from "../webviews/speedtest/speedtestPanelFactory";
Expand Down Expand Up @@ -94,15 +95,21 @@ export class ServiceContainer implements vscode.Disposable {
context.extensionUri,
this.logger,
);
// Shared by the sink (filename) and the service (event payload).
const sessionId = newSessionId();
const localJsonlSink = LocalJsonlSink.start(
{
baseDir: this.pathResolver.getTelemetryPath(),
sessionId: vscode.env.sessionId,
sessionId,
},
this.logger,
);
this.telemetryService = new TelemetryService(
const session = buildSession(
extractExtensionVersion(context.extension.packageJSON),
sessionId,
);
this.telemetryService = new TelemetryService(
session,
[localJsonlSink],
this.logger,
);
Expand Down
7 changes: 5 additions & 2 deletions src/telemetry/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,14 @@ export interface TelemetrySink {
}

/** Build session attributes from the extension version and ambient host data. */
export function buildSession(extensionVersion: string): SessionContext {
export function buildSession(
extensionVersion: string,
sessionId: string,
): SessionContext {
return {
extensionVersion,
machineId: vscode.env.machineId,
sessionId: vscode.env.sessionId,
sessionId,
osType: detectOsType(),
osVersion: os.release(),
hostArch: process.arch,
Expand Down
20 changes: 20 additions & 0 deletions src/telemetry/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { randomBytes } from "node:crypto";

// OTel-format ids: lowercase hex, no separators. Keeps a future OTel
// exporter a 1:1 mapping.

/** OTel `trace_id`: 16 bytes / 32 hex. */
export function newTraceId(): string {
return randomBytes(16).toString("hex");
}

/** OTel `span_id` (used as `event_id`): 8 bytes / 16 hex. */
export function newSpanId(): string {
return randomBytes(8).toString("hex");
}

/** Our own session id (16 bytes / 32 hex). Avoids `vscode.env.sessionId`,
* which is a UUID concatenated with a timestamp. */
export function newSessionId(): string {
return randomBytes(16).toString("hex");
}
14 changes: 7 additions & 7 deletions src/telemetry/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from "../settings/telemetry";

import {
buildSession,
buildErrorBlock,
type CallerMeasurements,
type CallerProperties,
Expand All @@ -17,6 +16,7 @@ import {
type TelemetryLevel,
type TelemetrySink,
} from "./event";
import { newSpanId, newTraceId } from "./ids";
import { NOOP_SPAN, type Span } from "./span";

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

public constructor(
extensionVersion: string,
session: SessionContext,
private readonly sinks: readonly TelemetrySink[],
private readonly logger: Logger,
) {
this.#session = buildSession(extensionVersion);
this.#session = session;
this.#level = readLevel();
this.#configWatcher = watchConfigurationChanges(
[{ setting: TELEMETRY_LEVEL_SETTING, getValue: readLevel }],
Expand Down Expand Up @@ -86,7 +86,7 @@ export class TelemetryService implements vscode.Disposable {
if (this.#level === "off") {
return;
}
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements);
this.#safeEmit(newSpanId(), eventName, properties, measurements);
}

public logError(
Expand All @@ -98,7 +98,7 @@ export class TelemetryService implements vscode.Disposable {
if (this.#level === "off") {
return;
}
this.#safeEmit(crypto.randomUUID(), eventName, properties, measurements, {
this.#safeEmit(newSpanId(), eventName, properties, measurements, {
error,
});
}
Expand All @@ -118,7 +118,7 @@ export class TelemetryService implements vscode.Disposable {
return fn(NOOP_SPAN);
}
return this.#startSpan(eventName, fn, properties, measurements, {
traceId: crypto.randomUUID(),
traceId: newTraceId(),
traceLevel: this.#level,
});
}
Expand All @@ -140,7 +140,7 @@ export class TelemetryService implements vscode.Disposable {
measurements: Record<string, number>,
spanOpts: SpanOptions,
): Promise<T> {
const eventId = crypto.randomUUID();
const eventId = newSpanId();
const { traceId, traceLevel } = spanOpts;
const span: Span = {
traceId,
Expand Down
3 changes: 2 additions & 1 deletion test/unit/core/commandManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CommandManager,
type CoderCommandId,
} from "@/core/commandManager";
import { buildSession } from "@/telemetry/event";
import { TelemetryService } from "@/telemetry/service";

import { TestSink } from "../../mocks/telemetry";
Expand All @@ -26,7 +27,7 @@ function makeHarness(): Harness {
config.set("coder.telemetry.level", "local");
const sink = new TestSink();
const telemetry = new TelemetryService(
"1.2.3-test",
buildSession("1.2.3-test", "test-session"),
[sink],
createMockLogger(),
);
Expand Down
6 changes: 3 additions & 3 deletions test/unit/telemetry/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
} from "@/telemetry/event";

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

expect(session).toMatchObject({
extensionVersion: "1.2.3-test",
machineId: "test-machine-id",
sessionId: "test-session-id",
sessionId: "session-abc",
platformName: "Visual Studio Code",
platformVersion: "1.106.0-test",
hostArch: process.arch,
Expand Down
16 changes: 8 additions & 8 deletions test/unit/telemetry/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

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

import { TestSink } from "../../mocks/telemetry";
Expand All @@ -8,9 +9,10 @@ import {
MockConfigurationProvider,
} from "../../mocks/testHelpers";

import type { TelemetrySink } from "@/telemetry/event";

const TEST_VERSION = "1.2.3-test";
const TEST_SESSION_ID = "test-session";

const testSession = () => buildSession(TEST_VERSION, TEST_SESSION_ID);

interface Harness {
service: TelemetryService;
Expand All @@ -23,7 +25,7 @@ function makeHarness(level: "off" | "local" = "local"): Harness {
config.set("coder.telemetry.level", level);
const sink = new TestSink();
const service = new TelemetryService(
TEST_VERSION,
testSession(),
[sink],
createMockLogger(),
);
Expand All @@ -32,7 +34,7 @@ function makeHarness(level: "off" | "local" = "local"): Harness {

function makeService(sinks: TelemetrySink[]): TelemetryService {
new MockConfigurationProvider().set("coder.telemetry.level", "local");
return new TelemetryService(TEST_VERSION, sinks, createMockLogger());
return new TelemetryService(testSession(), sinks, createMockLogger());
}

describe("TelemetryService", () => {
Expand All @@ -57,13 +59,11 @@ describe("TelemetryService", () => {
context: {
extensionVersion: "1.2.3-test",
machineId: "test-machine-id",
sessionId: "test-session-id",
sessionId: TEST_SESSION_ID,
deploymentUrl: "",
},
});
expect(event.eventId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
expect(event.eventId).toMatch(/^[0-9a-f]{16}$/);
expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});

Expand Down
Loading