Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
binary download/verify now emit local telemetry events with their duration
and outcome, so startup latency and failures are captured alongside other
local telemetry.
- Local telemetry now records `http.requests` rollups for per-route HTTP
health without emitting one event per request.

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"package:prerelease": "pnpm build:production && vsce package --pre-release --no-dependencies",
"storybook": "storybook dev -p 6006 --config-dir .storybook",
"storybook:build": "storybook build --config-dir .storybook",
"storybook:ci": "storybook build --test --config-dir .storybook",
"storybook:ci": "storybook build --test --config-dir .storybook",
"test": "cross-env CI=true ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs",
"test:extension": "cross-env ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project extension",
"test:integration": "pnpm compile-tests:integration && node esbuild.mjs && vscode-test",
Expand Down Expand Up @@ -211,7 +211,7 @@
]
},
"coder.telemetry.local": {
"markdownDescription": "Tunables for the local telemetry sink, which writes events as JSON Lines under the extension's global storage. Used when `#coder.telemetry.level#` is `local`. Missing or invalid fields fall back to defaults.",
"markdownDescription": "Advanced tunables for local telemetry collection. The local sink writes events as JSON Lines under the extension's global storage. Used when `#coder.telemetry.level#` is `local`. Missing or invalid fields fall back to defaults.",
"type": "object",
"additionalProperties": false,
"properties": {
Expand Down
87 changes: 56 additions & 31 deletions src/api/coderApi.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import {
Comment thread
EhabY marked this conversation as resolved.
type AxiosResponseHeaders,
type AxiosInstance,
isAxiosError,
type AxiosHeaders,
type AxiosInstance,
type AxiosResponseHeaders,
type AxiosResponseTransformer,
isAxiosError,
} from "axios";
import { Api } from "coder/site/src/api/api";
import {
type ServerSentEvent,
type GetInboxNotificationResponse,
type ProvisionerJobLog,
type Workspace,
type WorkspaceAgent,
type WorkspaceAgentLog,
} from "coder/site/src/api/typesGenerated";
import * as vscode from "vscode";
import { type ClientOptions } from "ws";

import { watchConfigurationChanges } from "../configWatcher";
import { ClientCertificateError } from "../error/clientCertificateError";
Expand All @@ -25,23 +16,22 @@ import { getHeaders } from "../headers";
import { EventStreamLogger } from "../logging/eventStreamLogger";
import {
createRequestMeta,
logRequest,
logError,
logRequest,
logResponse,
} from "../logging/httpLogger";
import { type Logger } from "../logging/logger";
import { HttpRequestsTelemetry } from "../logging/httpRequestsTelemetry";
import {
type RequestConfigWithMeta,
HttpClientLogLevel,
type RequestConfigWithMeta,
} from "../logging/types";
import { sizeOf } from "../logging/utils";
import { getHeaderCommand } from "../settings/headers";
import { HttpStatusCode, WebSocketCloseCode } from "../websocket/codes";
import {
type UnidirectionalStream,
type CloseEvent,
type ErrorEvent,
} from "../websocket/eventStreamConnection";
NOOP_TELEMETRY_REPORTER,
type TelemetryReporter,
} from "../telemetry/reporter";
import { HttpStatusCode, WebSocketCloseCode } from "../websocket/codes";
import {
OneWayWebSocket,
type OneWayWebSocketInit,
Expand All @@ -56,6 +46,23 @@ import { SseConnection } from "../websocket/sseConnection";
import { getRefreshCommand, refreshCertificates } from "./certificateRefresh";
import { createHttpAgent } from "./utils";

import type {
GetInboxNotificationResponse,
ProvisionerJobLog,
ServerSentEvent,
Workspace,
WorkspaceAgent,
WorkspaceAgentLog,
} from "coder/site/src/api/typesGenerated";
import type { ClientOptions } from "ws";

import type { Logger } from "../logging/logger";
import type {
CloseEvent,
ErrorEvent,
UnidirectionalStream,
} from "../websocket/eventStreamConnection";

const coderSessionTokenHeader = "Coder-Session-Token";

/**
Expand Down Expand Up @@ -86,24 +93,30 @@ export class CoderApi extends Api implements vscode.Disposable {
>();
private readonly configWatcher: vscode.Disposable;

private constructor(private readonly output: Logger) {
private constructor(
private readonly output: Logger,
private readonly httpRequestsTelemetry: HttpRequestsTelemetry,
) {
super();
this.configWatcher = this.watchConfigChanges();
}

/**
* Create a new CoderApi instance with the provided configuration.
* Automatically sets up logging interceptors and certificate handling.
* Automatically sets up logging interceptors, certificate handling,
* and HTTP request telemetry that emits via the given reporter.
*/
static create(
baseUrl: string,
token: string | undefined,
output: Logger,
telemetry: TelemetryReporter = NOOP_TELEMETRY_REPORTER,
): CoderApi {
const client = new CoderApi(output);
const httpRequestsTelemetry = new HttpRequestsTelemetry(telemetry);
const client = new CoderApi(output, httpRequestsTelemetry);
client.setCredentials(baseUrl, token);
Comment thread
EhabY marked this conversation as resolved.

setupInterceptors(client, output);
setupInterceptors(client, output, httpRequestsTelemetry);
return client;
}

Expand Down Expand Up @@ -155,6 +168,7 @@ export class CoderApi extends Api implements vscode.Disposable {
*/
dispose(): void {
this.configWatcher.dispose();
this.httpRequestsTelemetry.dispose();
for (const socket of this.reconnectingSockets) {
socket.close();
}
Expand Down Expand Up @@ -470,11 +484,16 @@ export class CoderApi extends Api implements vscode.Disposable {
}
}

/**
* Set up logging and request interceptors for the CoderApi instance.
*/
function setupInterceptors(client: CoderApi, output: Logger): void {
addLoggingInterceptors(client.getAxiosInstance(), output);
function setupInterceptors(
client: CoderApi,
output: Logger,
httpRequestsTelemetry: HttpRequestsTelemetry,
): void {
addRequestInterceptors(
client.getAxiosInstance(),
output,
httpRequestsTelemetry,
);

client.getAxiosInstance().interceptors.request.use(async (config) => {
const baseUrl = client.getAxiosInstance().defaults.baseURL;
Expand All @@ -499,7 +518,7 @@ function setupInterceptors(client: CoderApi, output: Logger): void {
return config;
});

// Wrap certificate errors and handle client certificate errors with refresh.
// Cert-refresh retries re-enter the chain, so each attempt is recorded.
client.getAxiosInstance().interceptors.response.use(
(r) => r,
async (err: unknown) => {
Expand All @@ -522,7 +541,11 @@ function setupInterceptors(client: CoderApi, output: Logger): void {
);
}

function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
function addRequestInterceptors(
client: AxiosInstance,
logger: Logger,
httpRequestsTelemetry: HttpRequestsTelemetry,
) {
client.interceptors.request.use(
(config) => {
const configWithMeta = config as RequestConfigWithMeta;
Expand Down Expand Up @@ -555,10 +578,12 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {

client.interceptors.response.use(
Comment thread
EhabY marked this conversation as resolved.
(response) => {
httpRequestsTelemetry.recordResponse(response);
logResponse(logger, response, getLogLevel());
return response;
},
(error: unknown) => {
httpRequestsTelemetry.recordError(error);
logError(logger, error, getLogLevel());
throw error;
},
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ async function doActivate(
const secretsManager = serviceContainer.getSecretsManager();
const contextManager = serviceContainer.getContextManager();
const commandManager = serviceContainer.getCommandManager();
const telemetryService = serviceContainer.getTelemetryService();

// Migrate auth storage from old flat format to new label-based format
await migrateAuthStorage(serviceContainer);
Expand Down Expand Up @@ -137,6 +138,7 @@ async function doActivate(
deployment?.url || "",
deploymentSessionAuth?.token,
output,
telemetryService,
);
ctx.subscriptions.push(client);

Expand Down
Loading
Loading