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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Gitpod",
"description": "Required to connect to Classic workspaces",
"publisher": "gitpod",
"version": "0.0.182",
"version": "0.0.183",
"license": "MIT",
"icon": "resources/gitpod.png",
"repository": {
Expand Down Expand Up @@ -420,7 +420,7 @@
"@types/js-yaml": "^4.0.5",
"@types/http-proxy-agent": "^2.0.1",
"@types/mocha": "^9.1.1",
"@types/node": "18.x",
"@types/node": "20.x",
"@types/proper-lockfile": "^4.1.2",
"@types/semver": "^7.3.10",
"@types/ssh2": "^0.5.52",
Expand All @@ -442,7 +442,7 @@
"mocha": "^10.0.0",
"ts-loader": "^9.2.7",
"ts-proto": "^1.140.0",
"typescript": "^4.6.3",
"typescript": "^5.7.3",
"webpack": "^5.42.0",
"webpack-cli": "^4.7.2"
},
Expand Down
13 changes: 11 additions & 2 deletions src/authentication/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ITelemetryService, UserFlowTelemetryProperties } from '../common/teleme
import { INotificationService } from '../services/notificationService';
import { ILogService } from '../services/logService';
import { Configuration } from '../configuration';
import { unwrapFetchError } from '../common/fetch';

interface SessionData {
id: string;
Expand Down Expand Up @@ -102,13 +103,21 @@ export default class GitpodAuthenticationProvider extends Disposable implements
try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 1500);
const resp = await fetch(endpoint, { signal: controller.signal });

let resp: Response;
try {
resp = await fetch(endpoint, { signal: controller.signal });
} catch (e) {
throw unwrapFetchError(e);
}

if (resp.ok) {
this._validScopes = (await resp.json()) as string[];
return this._validScopes;
}
} catch (e) {
this.logService.error(`Error fetching endpoint ${endpoint}`, e);
this.logService.error(`Error fetching endpoint ${endpoint}`);
this.logService.error(e);
}
return undefined;
}
Expand Down
29 changes: 19 additions & 10 deletions src/authentication/gitpodServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Disposable } from '../common/dispose';
import { INotificationService } from '../services/notificationService';
import { UserFlowTelemetryProperties } from '../common/telemetry';
import { ILogService } from '../services/logService';
import { unwrapFetchError } from '../common/fetch';

interface ExchangeTokenResponse {
token_type: 'Bearer';
Expand Down Expand Up @@ -150,16 +151,21 @@ export default class GitpodServer extends Disposable {

const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://gitpod.gitpod-desktop${GitpodServer.AUTH_COMPLETE_PATH}`));
try {
const exchangeTokenResponse = await fetch(`${this._serviceUrl}/api/oauth/token`, {
method: 'POST',
body: new URLSearchParams({
code,
grant_type: 'authorization_code',
client_id: `${vscode.env.uriScheme}-gitpod`,
redirect_uri: callbackUri.toString(true),
code_verifier: verifier
})
});
let exchangeTokenResponse:Response;
try {
exchangeTokenResponse = await fetch(`${this._serviceUrl}/api/oauth/token`, {
method: 'POST',
body: new URLSearchParams({
code,
grant_type: 'authorization_code',
client_id: `${vscode.env.uriScheme}-gitpod`,
redirect_uri: callbackUri.toString(true),
code_verifier: verifier
})
});
} catch (e) {
throw unwrapFetchError(e);
}

if (!exchangeTokenResponse.ok) {
this.notificationService.showErrorMessage(`Couldn't connect (token exchange): ${exchangeTokenResponse.statusText}, ${await exchangeTokenResponse.text()}`, { flow, id: 'failed_to_exchange' });
Expand All @@ -172,6 +178,9 @@ export default class GitpodServer extends Disposable {
const accessToken = JSON.parse(Buffer.from(jwtToken.split('.')[1], 'base64').toString())['jti'];
resolve(accessToken);
} catch (err) {
this.logService.error('Error exchanging code for token');
this.logService.error(err);

this.notificationService.showErrorMessage(`Couldn't connect (token exchange): ${err}`, { flow, id: 'failed_to_exchange' });
reject(err);
}
Expand Down
32 changes: 32 additions & 0 deletions src/common/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Gitpod. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// Collect error messages from nested errors as seen with Node's `fetch`.
function collectFetchErrorMessages(e: any): string {
const seen = new Set<any>();
function collect(e: any, indent: string): string {
if (!e || typeof e !== 'object' || seen.has(e)) {
return '';
}
seen.add(e);
const message = e.stack || e.message || e.code || e.toString?.() || '';
const messageStr = message.toString?.() as (string | undefined) || '';
return [
messageStr ? `${messageStr.split('\n').map(line => `${indent}${line}`).join('\n')}\n` : '',
collect(e.cause, indent + ' '),
...(Array.isArray(e.errors) ? e.errors.map((e: any) => collect(e, indent + ' ')) : []),
].join('');
}
return collect(e, '').trim();
}

export function unwrapFetchError(e: any) {
const err = new Error();
// Put collected messaged in the stack so vscode logger prints it
err.stack = collectFetchErrorMessages(e);
err.cause = undefined;
err.message = 'fetch error';
return err;
}
58 changes: 35 additions & 23 deletions src/common/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { ILogService } from '../services/logService';
import { unwrapFetchError } from './fetch';
import { isBuiltFromGHA } from './utils';
import fetch from 'node-fetch-commonjs';

const metricsHostMap = new Map<string, string>();

Expand All @@ -20,17 +20,23 @@ export async function addCounter(gitpodHost: string, name: string, labels: Recor
return;
}
const metricsHost = getMetricsHost(gitpodHost);
const resp = await fetch(
`https://${metricsHost}/metrics-api/metrics/counter/add/${name}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client': 'vscode-desktop-extension'
},
body: JSON.stringify(data)
}
);

let resp: Response;
try {
resp = await fetch(
`https://${metricsHost}/metrics-api/metrics/counter/add/${name}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client': 'vscode-desktop-extension'
},
body: JSON.stringify(data)
}
);
} catch (e) {
throw unwrapFetchError(e);
}

if (!resp.ok) {
throw new Error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
Expand All @@ -50,17 +56,23 @@ export async function addHistogram(gitpodHost: string, name: string, labels: Rec
return;
}
const metricsHost = getMetricsHost(gitpodHost);
const resp = await fetch(
`https://${metricsHost}/metrics-api/metrics/histogram/add/${name}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client': 'vscode-desktop-extension'
},
body: JSON.stringify(data)
}
);

let resp: Response;
try {
resp = await fetch(
`https://${metricsHost}/metrics-api/metrics/histogram/add/${name}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client': 'vscode-desktop-extension'
},
body: JSON.stringify(data)
}
);
} catch (e) {
throw unwrapFetchError(e);
}

if (!resp.ok) {
throw new Error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
Expand Down
25 changes: 12 additions & 13 deletions src/common/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as os from 'os';
import { Analytics, AnalyticsSettings } from '@segment/analytics-node';
import { ILogService } from '../services/logService';
import { cloneAndChange, escapeRegExpCharacters, isBuiltFromGHA, mixin } from '../common/utils';
import fetch from 'node-fetch-commonjs';
import { unwrapFetchError } from './fetch';

export const TRUSTED_VALUES = new Set([
'gitpodHost',
Expand Down Expand Up @@ -63,16 +63,6 @@ export function createSegmentAnalyticsClient(settings: AnalyticsSettings, gitpod
return client;
}


function getErrorMetricsEndpoint(gitpodHost: string): string {
try {
const serviceUrl = new URL(gitpodHost);
return `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
} catch {
throw new Error(`Invalid URL: ${gitpodHost}`);
}
}

export async function commonSendEventData(logService: ILogService, segmentClient: Analytics | undefined, machineId: string, eventName: string, data?: any): Promise<void> {
const properties = data ?? {};

Expand Down Expand Up @@ -125,7 +115,14 @@ export function commonSendErrorData(logService: ILogService, defaultGitpodHost:

// Unhandled errors have no data so use host from config
const gitpodHost = properties['gitpodHost'] ?? defaultGitpodHost;
const errorMetricsEndpoint = getErrorMetricsEndpoint(gitpodHost);
let errorMetricsEndpoint: string;
try {
const serviceUrl = new URL(gitpodHost);
errorMetricsEndpoint = `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
} catch {
logService.error(`Invalid gitpodHost: ${gitpodHost}`);
return;
}

properties['error_name'] = error.name;
properties['error_message'] = errorProps.message;
Expand Down Expand Up @@ -173,7 +170,9 @@ export function commonSendErrorData(logService: ILogService, defaultGitpodHost:
logService.error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
}
}).catch((e) => {
logService.error('Failed to report error to metrics endpoint!', e);
const err = unwrapFetchError(e);
logService.error('Failed to report error to metrics endpoint!');
logService.error(err);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default function isPlainObject(value: any) {
export class WrapError extends Error {
constructor(
msg: string,
readonly cause: any,
override readonly cause: any,
readonly code?: string
) {
super();
Expand Down
2 changes: 1 addition & 1 deletion src/local-ssh/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class WebSocketSSHProxy {
pipePromise = localSession.pipe(pipeSession);
return {};
}).catch(async err => {
this.logService.error('failed to authenticate proxy with username: ' + e.username ?? '', err);
this.logService.error('failed to authenticate proxy with username: ' + (e.username ?? ''), err);

this.flow.failureCode = getFailureCode(err);
let sendErrorReport = true;
Expand Down
9 changes: 8 additions & 1 deletion src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,14 @@ export class MetricsReporter {
if (this.intervalHandler) {
return;
}
this.intervalHandler = setInterval(() => this.report().catch(e => this.logger.error('Error while reporting metrics', e)), MetricsReporter.REPORT_INTERVAL);
this.intervalHandler = setInterval(async () => {
try {
await this.report()
} catch (e) {
this.logger.error('Error while reporting metrics');
this.logger.error(e);
}
}, MetricsReporter.REPORT_INTERVAL);
}

private async report() {
Expand Down
18 changes: 14 additions & 4 deletions src/remoteConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { IHostService } from './services/hostService';
import { WrapError, getServiceURL } from './common/utils';
import { IRemoteService } from './services/remoteService';
import { ExportLogsCommand } from './commands/logs';
import { unwrapFetchError } from './common/fetch';

export class RemoteConnector extends Disposable {

Expand Down Expand Up @@ -69,7 +70,13 @@ export class RemoteConnector extends Disposable {
const workspaceUrl = new URL((workspaceInfo as Workspace).status!.instance!.status!.url);

const sshHostKeyEndPoint = `https://${workspaceUrl.host}/_ssh/host_keys`;
const sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
let sshHostKeyResponse: Response;
try {
sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
} catch (e) {
throw unwrapFetchError(e);
}

if (!sshHostKeyResponse.ok) {
// Gitpod SSH gateway not configured
throw new NoSSHGatewayError(gitpodHost);
Expand Down Expand Up @@ -319,16 +326,19 @@ export class RemoteConnector extends Disposable {
}
this.telemetryService.sendUserFlowStatus('failed', { ...gatewayFlow, reason });
if (e instanceof NoRunningInstanceError) {
this.logService.error('No Running instance:', e);
this.logService.error('No Running instance:');
this.logService.error(e);
gatewayFlow['phase'] = e.phase;
this.notificationService.showErrorMessage(`Failed to connect to ${e.workspaceId} Gitpod workspace: workspace not running`, { flow: gatewayFlow, id: 'no_running_instance' });
return undefined;
} else {
if (e instanceof SSHError) {
this.logService.error('SSH test connection error:', e);
this.logService.error('SSH test connection error:');
} else {
this.logService.error(`Failed to connect to ${params.workspaceId} Gitpod workspace:`, e);
this.logService.error(`Failed to connect to ${params.workspaceId} Gitpod workspace:`);
}
this.logService.error(e);

const seeLogs = 'See Logs';
const showTroubleshooting = 'Show Troubleshooting';
this.notificationService.showErrorMessage(`Failed to connect to ${params.workspaceId} Gitpod workspace`, { flow: gatewayFlow, id: 'failed_to_connect' }, seeLogs, showTroubleshooting)
Expand Down
9 changes: 8 additions & 1 deletion src/services/remoteService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { INotificationService } from './notificationService';
import { getOpenSSHVersion } from '../ssh/nativeSSH';
import { retry } from '../common/async';
import { IStoredProfileExtension } from '../profileExtensions';
import { unwrapFetchError } from '../common/fetch';

export interface IRemoteService {
flow?: UserFlowTelemetryProperties;
Expand Down Expand Up @@ -243,7 +244,13 @@ export class RemoteService extends Disposable implements IRemoteService {

const wsUrl = new URL(workspaceUrl);
const sshHostKeyEndPoint = `https://${wsUrl.host}/_ssh/host_keys`;
const sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
let sshHostKeyResponse: Response;
try {
sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
} catch (e) {
throw unwrapFetchError(e);
}

if (!sshHostKeyResponse.ok) {
// Gitpod SSH gateway not configured
throw new NoSSHGatewayError(this.hostService.gitpodHost);
Expand Down
Loading