Skip to content

Commit 9fd73f7

Browse files
authored
Improve error logging for fetch error calls (#121)
1 parent 8d18258 commit 9fd73f7

File tree

13 files changed

+159
-94
lines changed

13 files changed

+159
-94
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Gitpod",
44
"description": "Required to connect to Classic workspaces",
55
"publisher": "gitpod",
6-
"version": "0.0.182",
6+
"version": "0.0.183",
77
"license": "MIT",
88
"icon": "resources/gitpod.png",
99
"repository": {
@@ -420,7 +420,7 @@
420420
"@types/js-yaml": "^4.0.5",
421421
"@types/http-proxy-agent": "^2.0.1",
422422
"@types/mocha": "^9.1.1",
423-
"@types/node": "18.x",
423+
"@types/node": "20.x",
424424
"@types/proper-lockfile": "^4.1.2",
425425
"@types/semver": "^7.3.10",
426426
"@types/ssh2": "^0.5.52",
@@ -442,7 +442,7 @@
442442
"mocha": "^10.0.0",
443443
"ts-loader": "^9.2.7",
444444
"ts-proto": "^1.140.0",
445-
"typescript": "^4.6.3",
445+
"typescript": "^5.7.3",
446446
"webpack": "^5.42.0",
447447
"webpack-cli": "^4.7.2"
448448
},

src/authentication/authentication.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ITelemetryService, UserFlowTelemetryProperties } from '../common/teleme
1313
import { INotificationService } from '../services/notificationService';
1414
import { ILogService } from '../services/logService';
1515
import { Configuration } from '../configuration';
16+
import { unwrapFetchError } from '../common/fetch';
1617

1718
interface SessionData {
1819
id: string;
@@ -102,13 +103,21 @@ export default class GitpodAuthenticationProvider extends Disposable implements
102103
try {
103104
const controller = new AbortController();
104105
setTimeout(() => controller.abort(), 1500);
105-
const resp = await fetch(endpoint, { signal: controller.signal });
106+
107+
let resp: Response;
108+
try {
109+
resp = await fetch(endpoint, { signal: controller.signal });
110+
} catch (e) {
111+
throw unwrapFetchError(e);
112+
}
113+
106114
if (resp.ok) {
107115
this._validScopes = (await resp.json()) as string[];
108116
return this._validScopes;
109117
}
110118
} catch (e) {
111-
this.logService.error(`Error fetching endpoint ${endpoint}`, e);
119+
this.logService.error(`Error fetching endpoint ${endpoint}`);
120+
this.logService.error(e);
112121
}
113122
return undefined;
114123
}

src/authentication/gitpodServer.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Disposable } from '../common/dispose';
1212
import { INotificationService } from '../services/notificationService';
1313
import { UserFlowTelemetryProperties } from '../common/telemetry';
1414
import { ILogService } from '../services/logService';
15+
import { unwrapFetchError } from '../common/fetch';
1516

1617
interface ExchangeTokenResponse {
1718
token_type: 'Bearer';
@@ -150,16 +151,21 @@ export default class GitpodServer extends Disposable {
150151

151152
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://gitpod.gitpod-desktop${GitpodServer.AUTH_COMPLETE_PATH}`));
152153
try {
153-
const exchangeTokenResponse = await fetch(`${this._serviceUrl}/api/oauth/token`, {
154-
method: 'POST',
155-
body: new URLSearchParams({
156-
code,
157-
grant_type: 'authorization_code',
158-
client_id: `${vscode.env.uriScheme}-gitpod`,
159-
redirect_uri: callbackUri.toString(true),
160-
code_verifier: verifier
161-
})
162-
});
154+
let exchangeTokenResponse:Response;
155+
try {
156+
exchangeTokenResponse = await fetch(`${this._serviceUrl}/api/oauth/token`, {
157+
method: 'POST',
158+
body: new URLSearchParams({
159+
code,
160+
grant_type: 'authorization_code',
161+
client_id: `${vscode.env.uriScheme}-gitpod`,
162+
redirect_uri: callbackUri.toString(true),
163+
code_verifier: verifier
164+
})
165+
});
166+
} catch (e) {
167+
throw unwrapFetchError(e);
168+
}
163169

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

src/common/fetch.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// Collect error messages from nested errors as seen with Node's `fetch`.
7+
function collectFetchErrorMessages(e: any): string {
8+
const seen = new Set<any>();
9+
function collect(e: any, indent: string): string {
10+
if (!e || typeof e !== 'object' || seen.has(e)) {
11+
return '';
12+
}
13+
seen.add(e);
14+
const message = e.stack || e.message || e.code || e.toString?.() || '';
15+
const messageStr = message.toString?.() as (string | undefined) || '';
16+
return [
17+
messageStr ? `${messageStr.split('\n').map(line => `${indent}${line}`).join('\n')}\n` : '',
18+
collect(e.cause, indent + ' '),
19+
...(Array.isArray(e.errors) ? e.errors.map((e: any) => collect(e, indent + ' ')) : []),
20+
].join('');
21+
}
22+
return collect(e, '').trim();
23+
}
24+
25+
export function unwrapFetchError(e: any) {
26+
const err = new Error();
27+
// Put collected messaged in the stack so vscode logger prints it
28+
err.stack = collectFetchErrorMessages(e);
29+
err.cause = undefined;
30+
err.message = 'fetch error';
31+
return err;
32+
}

src/common/metrics.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { ILogService } from '../services/logService';
7+
import { unwrapFetchError } from './fetch';
78
import { isBuiltFromGHA } from './utils';
8-
import fetch from 'node-fetch-commonjs';
99

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

@@ -20,17 +20,23 @@ export async function addCounter(gitpodHost: string, name: string, labels: Recor
2020
return;
2121
}
2222
const metricsHost = getMetricsHost(gitpodHost);
23-
const resp = await fetch(
24-
`https://${metricsHost}/metrics-api/metrics/counter/add/${name}`,
25-
{
26-
method: 'POST',
27-
headers: {
28-
'Content-Type': 'application/json',
29-
'X-Client': 'vscode-desktop-extension'
30-
},
31-
body: JSON.stringify(data)
32-
}
33-
);
23+
24+
let resp: Response;
25+
try {
26+
resp = await fetch(
27+
`https://${metricsHost}/metrics-api/metrics/counter/add/${name}`,
28+
{
29+
method: 'POST',
30+
headers: {
31+
'Content-Type': 'application/json',
32+
'X-Client': 'vscode-desktop-extension'
33+
},
34+
body: JSON.stringify(data)
35+
}
36+
);
37+
} catch (e) {
38+
throw unwrapFetchError(e);
39+
}
3440

3541
if (!resp.ok) {
3642
throw new Error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
@@ -50,17 +56,23 @@ export async function addHistogram(gitpodHost: string, name: string, labels: Rec
5056
return;
5157
}
5258
const metricsHost = getMetricsHost(gitpodHost);
53-
const resp = await fetch(
54-
`https://${metricsHost}/metrics-api/metrics/histogram/add/${name}`,
55-
{
56-
method: 'POST',
57-
headers: {
58-
'Content-Type': 'application/json',
59-
'X-Client': 'vscode-desktop-extension'
60-
},
61-
body: JSON.stringify(data)
62-
}
63-
);
59+
60+
let resp: Response;
61+
try {
62+
resp = await fetch(
63+
`https://${metricsHost}/metrics-api/metrics/histogram/add/${name}`,
64+
{
65+
method: 'POST',
66+
headers: {
67+
'Content-Type': 'application/json',
68+
'X-Client': 'vscode-desktop-extension'
69+
},
70+
body: JSON.stringify(data)
71+
}
72+
);
73+
} catch (e) {
74+
throw unwrapFetchError(e);
75+
}
6476

6577
if (!resp.ok) {
6678
throw new Error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);

src/common/telemetry.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as os from 'os';
77
import { Analytics, AnalyticsSettings } from '@segment/analytics-node';
88
import { ILogService } from '../services/logService';
99
import { cloneAndChange, escapeRegExpCharacters, isBuiltFromGHA, mixin } from '../common/utils';
10-
import fetch from 'node-fetch-commonjs';
10+
import { unwrapFetchError } from './fetch';
1111

1212
export const TRUSTED_VALUES = new Set([
1313
'gitpodHost',
@@ -63,16 +63,6 @@ export function createSegmentAnalyticsClient(settings: AnalyticsSettings, gitpod
6363
return client;
6464
}
6565

66-
67-
function getErrorMetricsEndpoint(gitpodHost: string): string {
68-
try {
69-
const serviceUrl = new URL(gitpodHost);
70-
return `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
71-
} catch {
72-
throw new Error(`Invalid URL: ${gitpodHost}`);
73-
}
74-
}
75-
7666
export async function commonSendEventData(logService: ILogService, segmentClient: Analytics | undefined, machineId: string, eventName: string, data?: any): Promise<void> {
7767
const properties = data ?? {};
7868

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

126116
// Unhandled errors have no data so use host from config
127117
const gitpodHost = properties['gitpodHost'] ?? defaultGitpodHost;
128-
const errorMetricsEndpoint = getErrorMetricsEndpoint(gitpodHost);
118+
let errorMetricsEndpoint: string;
119+
try {
120+
const serviceUrl = new URL(gitpodHost);
121+
errorMetricsEndpoint = `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
122+
} catch {
123+
logService.error(`Invalid gitpodHost: ${gitpodHost}`);
124+
return;
125+
}
129126

130127
properties['error_name'] = error.name;
131128
properties['error_message'] = errorProps.message;
@@ -173,7 +170,9 @@ export function commonSendErrorData(logService: ILogService, defaultGitpodHost:
173170
logService.error(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
174171
}
175172
}).catch((e) => {
176-
logService.error('Failed to report error to metrics endpoint!', e);
173+
const err = unwrapFetchError(e);
174+
logService.error('Failed to report error to metrics endpoint!');
175+
logService.error(err);
177176
});
178177
}
179178

src/common/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export default function isPlainObject(value: any) {
150150
export class WrapError extends Error {
151151
constructor(
152152
msg: string,
153-
readonly cause: any,
153+
override readonly cause: any,
154154
readonly code?: string
155155
) {
156156
super();

src/local-ssh/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class WebSocketSSHProxy {
209209
pipePromise = localSession.pipe(pipeSession);
210210
return {};
211211
}).catch(async err => {
212-
this.logService.error('failed to authenticate proxy with username: ' + e.username ?? '', err);
212+
this.logService.error('failed to authenticate proxy with username: ' + (e.username ?? ''), err);
213213

214214
this.flow.failureCode = getFailureCode(err);
215215
let sendErrorReport = true;

src/metrics.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,14 @@ export class MetricsReporter {
205205
if (this.intervalHandler) {
206206
return;
207207
}
208-
this.intervalHandler = setInterval(() => this.report().catch(e => this.logger.error('Error while reporting metrics', e)), MetricsReporter.REPORT_INTERVAL);
208+
this.intervalHandler = setInterval(async () => {
209+
try {
210+
await this.report()
211+
} catch (e) {
212+
this.logger.error('Error while reporting metrics');
213+
this.logger.error(e);
214+
}
215+
}, MetricsReporter.REPORT_INTERVAL);
209216
}
210217

211218
private async report() {

src/remoteConnector.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IHostService } from './services/hostService';
2727
import { WrapError, getServiceURL } from './common/utils';
2828
import { IRemoteService } from './services/remoteService';
2929
import { ExportLogsCommand } from './commands/logs';
30+
import { unwrapFetchError } from './common/fetch';
3031

3132
export class RemoteConnector extends Disposable {
3233

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

7172
const sshHostKeyEndPoint = `https://${workspaceUrl.host}/_ssh/host_keys`;
72-
const sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
73+
let sshHostKeyResponse: Response;
74+
try {
75+
sshHostKeyResponse = await fetch(sshHostKeyEndPoint);
76+
} catch (e) {
77+
throw unwrapFetchError(e);
78+
}
79+
7380
if (!sshHostKeyResponse.ok) {
7481
// Gitpod SSH gateway not configured
7582
throw new NoSSHGatewayError(gitpodHost);
@@ -319,16 +326,19 @@ export class RemoteConnector extends Disposable {
319326
}
320327
this.telemetryService.sendUserFlowStatus('failed', { ...gatewayFlow, reason });
321328
if (e instanceof NoRunningInstanceError) {
322-
this.logService.error('No Running instance:', e);
329+
this.logService.error('No Running instance:');
330+
this.logService.error(e);
323331
gatewayFlow['phase'] = e.phase;
324332
this.notificationService.showErrorMessage(`Failed to connect to ${e.workspaceId} Gitpod workspace: workspace not running`, { flow: gatewayFlow, id: 'no_running_instance' });
325333
return undefined;
326334
} else {
327335
if (e instanceof SSHError) {
328-
this.logService.error('SSH test connection error:', e);
336+
this.logService.error('SSH test connection error:');
329337
} else {
330-
this.logService.error(`Failed to connect to ${params.workspaceId} Gitpod workspace:`, e);
338+
this.logService.error(`Failed to connect to ${params.workspaceId} Gitpod workspace:`);
331339
}
340+
this.logService.error(e);
341+
332342
const seeLogs = 'See Logs';
333343
const showTroubleshooting = 'Show Troubleshooting';
334344
this.notificationService.showErrorMessage(`Failed to connect to ${params.workspaceId} Gitpod workspace`, { flow: gatewayFlow, id: 'failed_to_connect' }, seeLogs, showTroubleshooting)

0 commit comments

Comments
 (0)