From 34443431b1d34ea981e2c42056297045ce78fb9a Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Fri, 22 Sep 2023 16:56:01 +0200 Subject: [PATCH] add ws client metrics (#18773) --- components/dashboard/BUILD.yaml | 6 +- components/dashboard/src/service/config.json | 3 + components/dashboard/src/service/metrics.ts | 28 +-- components/dashboard/src/service/service.tsx | 44 +++- .../gitpod-protocol/src/gitpod-service.ts | 25 --- .../src/messaging/browser/connection.ts | 4 +- .../public-api/typescript/src/metrics.ts | 190 +++++++++++++----- components/supervisor/frontend/BUILD.yaml | 1 + components/supervisor/frontend/package.json | 7 +- .../src/ide/ide-metrics-service-client.ts | 22 +- .../frontend/src/ide/ide-web-socket.ts | 20 ++ components/supervisor/frontend/src/index.ts | 6 +- .../src/shared/frontend-dashboard-service.ts | 8 + .../pkg/components/ide-metrics/configmap.go | 31 +++ yarn.lock | 4 +- 15 files changed, 292 insertions(+), 107 deletions(-) create mode 100644 components/dashboard/src/service/config.json diff --git a/components/dashboard/BUILD.yaml b/components/dashboard/BUILD.yaml index af84257f2c9190..920211525274de 100644 --- a/components/dashboard/BUILD.yaml +++ b/components/dashboard/BUILD.yaml @@ -8,6 +8,7 @@ packages: - "src/**/*.svg" - "src/**/*.png" - "src/**/*.webp" + - "src/**/*.json" - "typings/**" - package.json - tailwind.config.js @@ -21,7 +22,10 @@ packages: - components/public-api/typescript:lib config: commands: - build: ["yarn", "build"] + build: + - sh + - -c + - yq w -i src/service/config.json commit commit-${__git_commit} -j && yarn build test: ["yarn", "test:unit"] yarnLock: ${coreYarnLockBase}/yarn.lock dontTest: false diff --git a/components/dashboard/src/service/config.json b/components/dashboard/src/service/config.json new file mode 100644 index 00000000000000..0a7dc2f1caa08c --- /dev/null +++ b/components/dashboard/src/service/config.json @@ -0,0 +1,3 @@ +{ + "commit": "" +} diff --git a/components/dashboard/src/service/metrics.ts b/components/dashboard/src/service/metrics.ts index 03e5af582f712f..2090603df441d7 100644 --- a/components/dashboard/src/service/metrics.ts +++ b/components/dashboard/src/service/metrics.ts @@ -8,21 +8,23 @@ import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url" import { MetricsReporter } from "@gitpod/public-api/lib/metrics"; import { getExperimentsClient } from "../experiments/client"; import { v4 } from "uuid"; +const commit = require("./config.json").commit; const originalConsoleError = console.error; -const options = { +const metricsReporter = new MetricsReporter({ gitpodUrl: new GitpodHostUrl(window.location.href).withoutWorkspacePrefix().toString(), clientName: "dashboard", - clientVersion: "", - logError: originalConsoleError.bind(console), + clientVersion: commit, + log: { + error: originalConsoleError.bind(console), + debug: console.debug.bind(console), + }, isEnabled: () => getExperimentsClient().getValueAsync("dashboard_metrics_enabled", false, {}), -}; -fetch("/api/version").then(async (res) => { - const version = await res.text(); - options.clientVersion = version; + commonErrorDetails: { + sessionId: v4(), + }, }); -const metricsReporter = new MetricsReporter(options); metricsReporter.startReporting(); window.addEventListener("unhandledrejection", (event) => { @@ -41,11 +43,12 @@ console.error = function (...args) { reportError(...args); }; -const commonDetails = { - sessionId: v4(), -}; export function updateCommonErrorDetails(update: { [key: string]: string | undefined }) { - Object.assign(commonDetails, update); + metricsReporter.updateCommonErrorDetails(update); +} + +export function instrumentWebSocket(ws: WebSocket, origin: string) { + metricsReporter.instrumentWebSocket(ws, origin); } export function reportError(...args: any[]) { @@ -88,7 +91,6 @@ export function reportError(...args: any[]) { ), ); } - data = Object.assign(data, commonDetails); if (err) { metricsReporter.reportError(err, data); diff --git a/components/dashboard/src/service/service.tsx b/components/dashboard/src/service/service.tsx index 37aed4d332c277..530f6d3dac2462 100644 --- a/components/dashboard/src/service/service.tsx +++ b/components/dashboard/src/service/service.tsx @@ -21,6 +21,8 @@ import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/fronten import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics"; import { helloService } from "./public-api"; import { getExperimentsClient } from "../experiments/client"; +import { ConnectError, Code } from "@bufbuild/connect"; +import { instrumentWebSocket } from "./metrics"; export const gitpodHostUrl = new GitpodHostUrl(window.location.toString()); @@ -28,6 +30,7 @@ function createGitpodService() { let host = gitpodHostUrl.asWebsocket().with({ pathname: GitpodServerPath }).withApi(); const connectionProvider = new WebSocketConnectionProvider(); + instrumentWebSocketConnection(connectionProvider); let numberOfErrors = 0; let onReconnect = () => {}; const proxy = connectionProvider.createProxy(host.toString(), undefined, { @@ -51,6 +54,23 @@ function createGitpodService() { return new GitpodServiceImpl(proxy, { onReconnect }); } +function instrumentWebSocketConnection(connectionProvider: WebSocketConnectionProvider): void { + const originalCreateWebSocket = connectionProvider["createWebSocket"]; + connectionProvider["createWebSocket"] = (url: string) => { + return originalCreateWebSocket.call( + connectionProvider, + url, + new Proxy(WebSocket, { + construct(target: any, argArray) { + const webSocket = new target(...argArray); + instrumentWebSocket(webSocket, "gitpod"); + return webSocket; + }, + }), + ); + }; +} + export function getGitpodService(): GitpodService { const w = window as any; const _gp = w._gp || (w._gp = {}); @@ -125,16 +145,28 @@ function testPublicAPI(service: any): void { if (isTest) { try { let previousCount = 0; - for await (const reply of helloService.lotsOfReplies({ previousCount })) { + for await (const reply of helloService.lotsOfReplies( + { previousCount }, + { + // GCP timeout is 10 minutes, we timeout 3 mins earlier + // to avoid unknown network errors + timeoutMs: 7 * 60 * 1000, + }, + )) { previousCount = reply.count; backoff = BASE_BACKOFF; } } catch (e) { - console.error(e, { - userId: user?.id, - grpcType, - }); - backoff = Math.min(2 * backoff, MAX_BACKOFF); + if (ConnectError.from(e).code === Code.Canceled) { + // timeout is expected, continue as usual + backoff = BASE_BACKOFF; + } else { + backoff = Math.min(2 * backoff, MAX_BACKOFF); + console.error(e, { + userId: user?.id, + grpcType, + }); + } } } else { backoff = BASE_BACKOFF; diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 514874687d002a..ed18bd5ef8e295 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -54,8 +54,6 @@ import { WorkspaceInstanceRepoStatus, } from "./workspace-instance"; import { AdminServer } from "./admin-protocol"; -import { GitpodHostUrl } from "./util/gitpod-host-url"; -import { WebSocketConnectionProvider } from "./messaging/browser/connection"; import { Emitter } from "./util/event"; import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./analytics"; import { IDEServer } from "./ide-protocol"; @@ -712,26 +710,3 @@ export class GitpodServiceImpl( - serverUrl: string | Promise, -) { - const toWsUrl = (serverUrl: string) => { - return new GitpodHostUrl(serverUrl).asWebsocket().withApi({ pathname: GitpodServerPath }).toString(); - }; - let url: string | Promise; - if (typeof serverUrl === "string") { - url = toWsUrl(serverUrl); - } else { - url = serverUrl.then((url) => toWsUrl(url)); - } - - const connectionProvider = new WebSocketConnectionProvider(); - let onReconnect = () => {}; - const gitpodServer = connectionProvider.createProxy(url, undefined, { - onListening: (socket) => { - onReconnect = () => socket.reconnect(); - }, - }); - return new GitpodServiceImpl(gitpodServer, { onReconnect }); -} diff --git a/components/gitpod-protocol/src/messaging/browser/connection.ts b/components/gitpod-protocol/src/messaging/browser/connection.ts index 2b7d73d7d5d851..b58494781e3fc3 100644 --- a/components/gitpod-protocol/src/messaging/browser/connection.ts +++ b/components/gitpod-protocol/src/messaging/browser/connection.ts @@ -87,14 +87,14 @@ export class WebSocketConnectionProvider { /** * Creates a web socket for the given url */ - createWebSocket(url: string): WebSocket { + createWebSocket(url: string, WebSocketConstructor = WebSocket): WebSocket { return new ReconnectingWebSocket(url, undefined, { maxReconnectionDelay: 10000, minReconnectionDelay: 1000, reconnectionDelayGrowFactor: 1.3, maxRetries: Infinity, debug: false, - WebSocket: WebSocket, + WebSocket: WebSocketConstructor, }) as any; } } diff --git a/components/public-api/typescript/src/metrics.ts b/components/public-api/typescript/src/metrics.ts index ba71d7d285d1cf..b18e9e3cab893c 100644 --- a/components/public-api/typescript/src/metrics.ts +++ b/components/public-api/typescript/src/metrics.ts @@ -52,6 +52,8 @@ class PrometheusClientCallMetrics { readonly handledCounter: PromCounter; readonly handledSecondsHistogram: PromHistorgram; + readonly webSocketCounter: PromCounter; + constructor() { this.startedCounter = new Counter({ name: "grpc_client_started_total", @@ -84,6 +86,13 @@ class PrometheusClientCallMetrics { buckets: [0.1, 0.2, 0.5, 1, 2, 5, 10], // it should be aligned with https://github.com/gitpod-io/gitpod/blob/84ed1a0672d91446ba33cb7b504cfada769271a8/install/installer/pkg/components/ide-metrics/configmap.go#L315 registers: [register], }); + + this.webSocketCounter = new Counter({ + name: "websocket_client_total", + help: "Total number of WebSocket connections by the client", + labelNames: ["origin", "instance_phase", "status", "code", "was_clean"], + registers: [register], + }); } started(labels: IGrpcCallMetricsLabels): void { @@ -140,7 +149,7 @@ class PrometheusClientCallMetrics { } } -const GRPCMetrics = new PrometheusClientCallMetrics(); +const metrics = new PrometheusClientCallMetrics(); export function getMetricsInterceptor(): Interceptor { const getLabels = (req: UnaryRequest | StreamRequest): IGrpcCallMetricsLabels => { @@ -185,14 +194,14 @@ export function getMetricsInterceptor(): Interceptor { } finally { if (handleMetrics && !settled) { stopTimer({ grpc_code: status ? Code[status] : "OK" }); - GRPCMetrics.handled({ ...labels, code: status ? Code[status] : "OK" }); + metrics.handled({ ...labels, code: status ? Code[status] : "OK" }); } } } const labels = getLabels(req); - GRPCMetrics.started(labels); - const stopTimer = GRPCMetrics.startHandleTimer(labels); + metrics.started(labels); + const stopTimer = metrics.startHandleTimer(labels); let settled = false; let status: Code | undefined; @@ -203,11 +212,7 @@ export function getMetricsInterceptor(): Interceptor { } else { request = { ...req, - message: incrementStreamMessagesCounter( - req.message, - GRPCMetrics.sent.bind(GRPCMetrics, labels), - false, - ), + message: incrementStreamMessagesCounter(req.message, metrics.sent.bind(metrics, labels), false), }; } @@ -220,11 +225,7 @@ export function getMetricsInterceptor(): Interceptor { } else { response = { ...res, - message: incrementStreamMessagesCounter( - res.message, - GRPCMetrics.received.bind(GRPCMetrics, labels), - true, - ), + message: incrementStreamMessagesCounter(res.message, metrics.received.bind(metrics, labels), true), }; } @@ -237,12 +238,14 @@ export function getMetricsInterceptor(): Interceptor { } finally { if (settled) { stopTimer({ grpc_code: status ? Code[status] : "OK" }); - GRPCMetrics.handled({ ...labels, code: status ? Code[status] : "OK" }); + metrics.handled({ ...labels, code: status ? Code[status] : "OK" }); } } }; } +export type MetricsRequest = RequestInit & { url: string }; + export class MetricsReporter { private static readonly REPORT_INTERVAL = 10000; @@ -250,24 +253,36 @@ export class MetricsReporter { private readonly metricsHost: string; + private sendQueue = Promise.resolve(); + + private readonly pendingRequests: MetricsRequest[] = []; + constructor( private readonly options: { gitpodUrl: string; clientName: string; clientVersion: string; - logError: typeof console.error; - isEnabled: () => Promise; + log: { + error: typeof console.error; + debug: typeof console.debug; + }; + isEnabled?: () => Promise; + commonErrorDetails: { [key: string]: string | undefined }; }, ) { this.metricsHost = `ide.${new URL(options.gitpodUrl).hostname}`; } + updateCommonErrorDetails(update: { [key: string]: string | undefined }) { + Object.assign(this.options.commonErrorDetails, update); + } + startReporting() { if (this.intervalHandler) { return; } this.intervalHandler = setInterval( - () => this.report().catch((e) => this.options.logError("metrics: error while reporting", e)), + () => this.report().catch((e) => this.options.log.error("metrics: error while reporting", e)), MetricsReporter.REPORT_INTERVAL, ); } @@ -278,11 +293,19 @@ export class MetricsReporter { } } + private async isEnabled(): Promise { + if (!this.options.isEnabled) { + return true; + } + return this.options.isEnabled(); + } + private async report() { - const enabled = await this.options.isEnabled(); + const enabled = await this.isEnabled(); if (!enabled) { return; } + const metrics = await register.getMetricsAsJSON(); register.resetMetrics(); for (const m of metrics) { @@ -298,16 +321,25 @@ export class MetricsReporter { this.syncReportHistogram(m); } } + + while (this.pendingRequests.length) { + const request = this.pendingRequests.shift(); + if (request) { + this.send(request); + } + } } private syncReportCounter(metric: MetricObjectWithValues>) { for (const { value, labels } of metric.values) { if (value > 0) { - this.post("metrics/counter/add/" + metric.name, { - name: metric.name, - labels, - value, - }); + this.push( + this.create("metrics/counter/add/" + metric.name, { + name: metric.name, + labels, + value, + }), + ); } } } @@ -330,13 +362,15 @@ export class MetricsReporter { sum = value; } else if (metricName.endsWith("_count")) { if (value > 0) { - this.post("metrics/histogram/add/" + metric.name, { - name: metric.name, - labels, - count: value, - sum, - buckets, - }); + this.push( + this.create("metrics/histogram/add/" + metric.name, { + name: metric.name, + labels, + count: value, + sum, + buckets, + }), + ); } sum = 0; buckets = []; @@ -365,11 +399,11 @@ export class MetricsReporter { [key: string]: string | undefined; }, ): Promise { - const enabled = await this.options.isEnabled(); + const enabled = await this.isEnabled(); if (!enabled) { return; } - const properties = { ...data }; + const properties = { ...data, ...this.options.commonErrorDetails }; properties["error_name"] = error.name; properties["error_message"] = error.message; @@ -381,20 +415,23 @@ export class MetricsReporter { delete properties["instanceId"]; delete properties["userId"]; - await this.post("reportError", { - component: this.options.clientName, - errorStack: error.stack ?? String(error), - version: this.options.clientVersion, - workspaceId: workspaceId ?? "", - instanceId: instanceId ?? "", - userId: userId ?? "", - properties, - }); + await this.send( + this.create("reportError", { + component: this.options.clientName, + errorStack: error.stack ?? String(error), + version: this.options.clientVersion, + workspaceId: workspaceId ?? "", + instanceId: instanceId ?? "", + userId: userId ?? "", + properties, + }), + ); } - private async post(endpoint: string, data: any): Promise { + private create(endpoint: string, data: any): MetricsRequest | undefined { try { - const response = await fetch(`https://${this.metricsHost}/metrics-api/` + endpoint, { + return { + url: `https://${this.metricsHost}/metrics-api/` + endpoint, method: "POST", headers: { "Content-Type": "application/json", @@ -403,13 +440,66 @@ export class MetricsReporter { }, body: JSON.stringify(data), credentials: "omit", - }); - - if (!response.ok) { - this.options.logError(`metrics: endpoint responded with ${response.status} ${response.statusText}`); - } + }; } catch (e) { - this.options.logError("metrics: failed to post", e); + this.options.log.error("metrics: failed to create request", e); + return undefined; + } + } + + private push(request: MetricsRequest | undefined): void { + if (!request) { + return; + } + this.pendingRequests.push(request); + } + + private async send(request: MetricsRequest | undefined): Promise { + if (!request) { + return request; } + this.sendQueue = this.sendQueue.then(async () => { + try { + const response = await fetch(request.url, request); + if (!response.ok) { + this.options.log.error( + `metrics: endpoint responded with ${response.status} ${response.statusText}`, + ); + } + } catch (e) { + this.options.log.debug("metrics: failed to post, trying again next time", e); + this.push(request); + } + }); + await this.sendQueue; + } + + instrumentWebSocket(ws: WebSocket, origin: string) { + const inc = (status: string, code?: number, wasClean?: boolean) => { + metrics.webSocketCounter + .labels({ + origin, + instance_phase: this.options.commonErrorDetails["instancePhase"], + status, + code: code !== undefined ? String(code) : undefined, + was_clean: wasClean !== undefined ? String(Number(wasClean)) : undefined, + }) + .inc(); + }; + inc("new"); + ws.addEventListener("open", () => inc("open")); + ws.addEventListener("error", (event) => { + inc("error"); + this.reportError(new Error(`WebSocket failed: ${String(event)}`)); + }); + ws.addEventListener("close", (event) => { + inc("close", event.code, event.wasClean); + if (!event.wasClean) { + this.reportError(new Error("WebSocket was not closed cleanly"), { + code: String(event.code), + reason: event.reason, + }); + } + }); } } diff --git a/components/supervisor/frontend/BUILD.yaml b/components/supervisor/frontend/BUILD.yaml index 832d0fe9136c3b..68d7404b6e08c2 100644 --- a/components/supervisor/frontend/BUILD.yaml +++ b/components/supervisor/frontend/BUILD.yaml @@ -12,6 +12,7 @@ packages: - components/gitpod-protocol:lib - components/supervisor-api/typescript-grpc:lib - components/ide-metrics-api/typescript-grpcweb:lib + - components/public-api/typescript:lib config: dontTest: true yarnLock: ${coreYarnLockBase}/../yarn.lock diff --git a/components/supervisor/frontend/package.json b/components/supervisor/frontend/package.json index 64d4e471643de4..1d687900a46219 100644 --- a/components/supervisor/frontend/package.json +++ b/components/supervisor/frontend/package.json @@ -5,17 +5,20 @@ "version": "0.0.0", "dependencies": { "@gitpod/gitpod-protocol": "0.1.5", + "@gitpod/public-api": "0.1.5", "@gitpod/supervisor-api-grpc": "0.1.5", + "buffer": "^4.3.0", "crypto-browserify": "3.12.0", + "process": "^0.11.10", "stream-browserify": "^2.0.1", "url": "^0.11.1", "util": "^0.11.1", - "buffer": "^4.3.0", - "process": "^0.11.10" + "uuid": "8.3.2" }, "devDependencies": { "@types/sharedworker": "^0.0.29", "@types/trusted-types": "^2.0.0", + "@types/uuid": "8.3.1", "concurrently": "^6.2.1", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", diff --git a/components/supervisor/frontend/src/ide/ide-metrics-service-client.ts b/components/supervisor/frontend/src/ide/ide-metrics-service-client.ts index 0dce1d3762738f..5cf652ae0c8ac5 100644 --- a/components/supervisor/frontend/src/ide/ide-metrics-service-client.ts +++ b/components/supervisor/frontend/src/ide/ide-metrics-service-client.ts @@ -4,9 +4,27 @@ * See License.AGPL.txt in the project root for license information. */ -import { FrontendDashboardServiceClient } from "../shared/frontend-dashboard-service"; import { serverUrl, workspaceUrl } from "../shared/urls"; const commit = require("../../config.json").commit; +import { v4 } from "uuid"; + +import { MetricsReporter } from "@gitpod/public-api/lib/metrics"; + +export const metricsReporter = new MetricsReporter({ + gitpodUrl: serverUrl.toString(), + clientName: "supervisor-frontend", + clientVersion: commit, + log: console, + commonErrorDetails: { + sessionId: v4(), + }, +}); +metricsReporter.startReporting(); + +import { FrontendDashboardServiceClient } from "../shared/frontend-dashboard-service"; + +// TODO(ak) migrate to MetricsReporter +const MetricsUrl = serverUrl.asIDEMetrics().toString(); export enum MetricsName { SupervisorFrontendClientTotal = "gitpod_supervisor_frontend_client_total", @@ -14,8 +32,6 @@ export enum MetricsName { SupervisorFrontendLoadTotal = "gitpod_vscode_web_load_total", } -const MetricsUrl = serverUrl.asIDEMetrics().toString(); - interface AddCounterParam { value?: number; labels?: Record; diff --git a/components/supervisor/frontend/src/ide/ide-web-socket.ts b/components/supervisor/frontend/src/ide/ide-web-socket.ts index fa249d07747a8f..56aa70453dd7fc 100644 --- a/components/supervisor/frontend/src/ide/ide-web-socket.ts +++ b/components/supervisor/frontend/src/ide/ide-web-socket.ts @@ -4,6 +4,8 @@ * See License.AGPL.txt in the project root for license information. */ +import { serverUrl } from "../shared/urls"; +import { metricsReporter } from "./ide-metrics-service-client"; import ReconnectingWebSocket from "reconnecting-websocket"; import { Disposable } from "@gitpod/gitpod-protocol/lib/util/disposable"; @@ -11,12 +13,23 @@ let connected = false; const workspaceSockets = new Set(); const workspaceOrigin = new URL(window.location.href).origin; +const gitpodOrigin = new URL(serverUrl.toString()).origin; const WebSocket = window.WebSocket; function isWorkspaceOrigin(url: string): boolean { const originUrl = new URL(url); originUrl.protocol = window.location.protocol; return originUrl.origin === workspaceOrigin; } +function isLocalhostOrigin(url: string): boolean { + const originUrl = new URL(url); + originUrl.protocol = window.location.protocol; + return originUrl.hostname === "localhost"; +} +function isGitpodOrigin(url: string): boolean { + const originUrl = new URL(url); + originUrl.protocol = window.location.protocol; + return originUrl.origin === gitpodOrigin; +} /** * IDEWebSocket is a proxy to standard WebSocket * which allows to control when web sockets to the workspace @@ -31,12 +44,19 @@ class IDEWebSocket extends ReconnectingWebSocket { maxRetries: 0, connectionTimeout: 2147483647, // disable connection timeout, clients should handle it }); + let origin = "unknown"; if (isWorkspaceOrigin(url)) { + origin = "workspace"; workspaceSockets.add(this); this.addEventListener("close", () => { workspaceSockets.delete(this); }); + } else if (isLocalhostOrigin(url)) { + origin = "localhost"; + } else if (isGitpodOrigin(url)) { + origin = "gitpod"; } + metricsReporter.instrumentWebSocket(this as any, origin); } static disconnectWorkspace(): void { for (const socket of workspaceSockets) { diff --git a/components/supervisor/frontend/src/index.ts b/components/supervisor/frontend/src/index.ts index f650da33ffa859..e8a97172cef969 100644 --- a/components/supervisor/frontend/src/index.ts +++ b/components/supervisor/frontend/src/index.ts @@ -256,7 +256,7 @@ LoadingFrame.load().then(async (loading) => { } if (!isWorkspaceInstancePhase("running")) { if (debugWorkspace && frontendDashboardServiceClient.latestInfo) { - window.open('', '_self')?.close() + window.open("", "_self")?.close(); } await new Promise((resolve) => { frontendDashboardServiceClient.onInfoUpdate((status) => { @@ -269,8 +269,8 @@ LoadingFrame.load().then(async (loading) => { const supervisorServiceClient = SupervisorServiceClient.get(); if (debugWorkspace) { supervisorServiceClient.supervisorWillShutdown.then(() => { - window.open('', '_self')?.close() - }) + window.open("", "_self")?.close(); + }); } const [ideStatus] = await Promise.all([ supervisorServiceClient.ideReady, diff --git a/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts b/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts index 4771bf3a039c4e..e02d0bf791aeb4 100644 --- a/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts +++ b/components/supervisor/frontend/src/shared/frontend-dashboard-service.ts @@ -9,6 +9,7 @@ import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/fronten import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics"; import { Emitter } from "@gitpod/gitpod-protocol/lib/util/event"; import { workspaceUrl, serverUrl } from "./urls"; +import { metricsReporter } from "../ide/ide-metrics-service-client"; export class FrontendDashboardServiceClient implements IDEFrontendDashboardService.IClient { public latestInfo!: IDEFrontendDashboardService.Info; @@ -31,6 +32,13 @@ export class FrontendDashboardServiceClient implements IDEFrontendDashboardServi return; } if (IDEFrontendDashboardService.isInfoUpdateEventData(event.data)) { + metricsReporter.updateCommonErrorDetails({ + userId: event.data.info.loggedUserId, + ownerId: event.data.info.ownerId, + workspaceId: event.data.info.workspaceID, + instanceId: event.data.info.instanceId, + instancePhase: event.data.info.statusPhase, + }); this.version = event.data.version; this.latestInfo = event.data.info; if (event.data.info.credentialsToken?.length > 0) { diff --git a/install/installer/pkg/components/ide-metrics/configmap.go b/install/installer/pkg/components/ide-metrics/configmap.go index 4730e9509238f2..8c556cf793dd9f 100644 --- a/install/installer/pkg/components/ide-metrics/configmap.go +++ b/install/installer/pkg/components/ide-metrics/configmap.go @@ -305,6 +305,37 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { DefaultValue: "unknown", }, }, + { + Name: "websocket_client_total", + Help: "Total number of WebSocket connections by the client", + Labels: []config.LabelAllowList{ + { + Name: "origin", + AllowValues: []string{"unknown", "workspace", "gitpod", "localhost"}, + DefaultValue: "unknown", + }, + { + Name: "instance_phase", + AllowValues: []string{"undefined", "unknown", "preparing", "building", "pending", "creating", "initializing", "running", "interrupted", "stopping", "stopped"}, + DefaultValue: "undefined", + }, + { + Name: "status", + AllowValues: []string{"unknown", "new", "open", "error", "close"}, + DefaultValue: "unknown", + }, + { + Name: "code", + AllowValues: []string{"*"}, + DefaultValue: "unknown", + }, + { + Name: "was_clean", + AllowValues: []string{"unknown", "0", "1"}, + DefaultValue: "unknown", + }, + }, + }, } histogramMetrics := []config.HistogramMetricsConfiguration{ diff --git a/yarn.lock b/yarn.lock index 4b3617b474db86..c366296c710499 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3477,7 +3477,7 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== -"@types/uuid@^8.3.1": +"@types/uuid@8.3.1", "@types/uuid@^8.3.1": version "8.3.1" resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz" integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== @@ -14009,7 +14009,7 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^8.3.2: +uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==