Skip to content

Commit 25d686c

Browse files
jeanp413mustard-mh
andauthored
Add error report (#39)
* Add error report --------- Co-authored-by: hwen <[email protected]>
1 parent 751dbc6 commit 25d686c

File tree

4 files changed

+110
-98
lines changed

4 files changed

+110
-98
lines changed

src/common/telemetry.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -222,17 +222,6 @@ export class BaseTelemetryReporter extends Disposable {
222222
return ret;
223223
}
224224

225-
/**
226-
* Whether or not it is safe to send error telemetry
227-
*/
228-
private shouldSendErrorTelemetry(): boolean {
229-
if (this.errorOptIn === false) {
230-
return false;
231-
}
232-
233-
return true;
234-
}
235-
236225
// __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
237226
// __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
238227
// __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
@@ -411,25 +400,14 @@ export class BaseTelemetryReporter extends Disposable {
411400
* Given an event name, some properties, and measurements sends an error event
412401
* @param eventName The name of the event
413402
* @param properties The properties to send with the event
414-
* @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized
415403
*/
416-
public sendTelemetryErrorEvent(eventName: string, properties?: { [key: string]: string }, errorProps?: string[]): void {
404+
public sendTelemetryErrorEvent(eventName: string, properties?: { [key: string]: string }): void {
417405
if (this.errorOptIn && eventName !== '') {
418406
// always clean the properties if first party
419407
// do not send any error properties if we shouldn't send error telemetry
420408
// if we have no errorProps, assume all are error props
421409
properties = { ...properties, ...this.getCommonProperties() };
422-
const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => {
423-
if (this.shouldSendErrorTelemetry()) {
424-
return this.anonymizeFilePaths(prop, false);
425-
}
426-
427-
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
428-
return 'REDACTED';
429-
}
430-
431-
return this.anonymizeFilePaths(prop, false);
432-
});
410+
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, false));
433411
this.telemetryAppender.logEvent(`${eventName}`, { properties: this.removePropertiesWithPossibleUserInfo(cleanProperties) });
434412
}
435413
}
@@ -440,7 +418,7 @@ export class BaseTelemetryReporter extends Disposable {
440418
* @param properties The properties to send with the event
441419
*/
442420
public sendTelemetryException(error: Error, properties?: TelemetryEventProperties): void {
443-
if (this.shouldSendErrorTelemetry() && this.errorOptIn && error) {
421+
if (this.errorOptIn && error) {
444422
properties = { ...properties, ...this.getCommonProperties() };
445423
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, false));
446424
// Also run the error stack through the anonymizer

src/heartbeat.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class HeartbeatManager extends Disposable {
2929
readonly workspaceId: string,
3030
readonly instanceId: string,
3131
readonly debugWorkspace: boolean,
32-
private readonly accessToken: string,
32+
private readonly session: vscode.AuthenticationSession,
3333
private readonly publicApi: GitpodPublicApi | undefined,
3434
private readonly logger: vscode.LogOutputChannel,
3535
private readonly telemetry: TelemetryReporter
@@ -90,7 +90,7 @@ export class HeartbeatManager extends Disposable {
9090
}
9191
}));
9292

93-
this.logger.debug(`Heartbeat manager for workspace ${workspaceId} (${instanceId}) - ${gitpodHost} started`);
93+
this.logger.info(`Heartbeat manager for workspace ${workspaceId} (${instanceId}) - ${gitpodHost} started`);
9494

9595
// Start heartbeating interval
9696
this.sendHeartBeat();
@@ -109,8 +109,8 @@ export class HeartbeatManager extends Disposable {
109109
}
110110

111111
private updateLastActivity(event: string, document?: vscode.TextDocument) {
112-
this.lastActivity = new Date().getTime();
113-
this.lastActivityEvent = event;
112+
this.lastActivity = new Date().getTime();
113+
this.lastActivityEvent = event;
114114

115115
const eventName = document ? `${event}:${document.uri.scheme}` : event;
116116

@@ -120,7 +120,7 @@ export class HeartbeatManager extends Disposable {
120120

121121
private async sendHeartBeat(wasClosed?: true) {
122122
try {
123-
await withServerApi(this.accessToken, this.gitpodHost, async service => {
123+
await withServerApi(this.session.accessToken, this.gitpodHost, async service => {
124124
const workspaceInfo = this.publicApi
125125
? await this.publicApi.getWorkspace(this.workspaceId)
126126
: await service.server.getWorkspace(this.workspaceId);
@@ -142,9 +142,11 @@ export class HeartbeatManager extends Disposable {
142142
this.stopHeartbeat();
143143
}
144144
}, this.logger);
145-
} catch (err) {
145+
} catch (e) {
146146
const suffix = wasClosed ? 'closed heartbeat' : 'heartbeat';
147-
this.logger.error(`Failed to send ${suffix}, triggered by ${this.lastActivityEvent} event:`, err);
147+
e.message = `Failed to send ${suffix}, triggered by event: ${this.lastActivityEvent}: ${e.message}`;
148+
this.logger.error(e);
149+
this.telemetry.sendTelemetryException(e, { workspaceId: this.workspaceId, instanceId: this.instanceId, userId: this.session.account.id });
148150
}
149151
}
150152

src/remoteConnector.ts

Lines changed: 56 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ interface SSHConnectionParams {
4949
}
5050

5151
interface SSHConnectionInfo extends SSHConnectionParams {
52-
isFirstConnection: boolean;
5352
}
5453

5554
interface WorkspaceRestartInfo {
@@ -144,12 +143,13 @@ export default class RemoteConnector extends Disposable {
144143
) {
145144
super();
146145

147-
if (isGitpodRemoteWindow(context)) {
146+
const remoteConnectionInfo = getGitpodRemoteWindow(context);
147+
if (remoteConnectionInfo) {
148148
this._register(vscode.commands.registerCommand('gitpod.api.autoTunnel', this.autoTunnelCommand, this));
149149

150150
// Don't await this on purpose so it doesn't block extension activation.
151151
// Internally requesting a Gitpod Session requires the extension to be already activated.
152-
this.onGitpodRemoteConnection();
152+
this.onGitpodRemoteConnection(remoteConnectionInfo);
153153
} else {
154154
this.checkForStoppedWorkspaces();
155155
}
@@ -833,7 +833,7 @@ export default class RemoteConnector extends Disposable {
833833

834834
await this.updateRemoteSSHConfig(usingSSHGateway, localAppSSHConfigPath);
835835

836-
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestination!.toRemoteSSHString()}`, { ...params, isFirstConnection: true } as SSHConnectionParams);
836+
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestination!.toRemoteSSHString()}`, { ...params } as SSHConnectionParams);
837837

838838
const forceNewWindow = this.context.extensionMode === vscode.ExtensionMode.Production;
839839

@@ -888,7 +888,7 @@ export default class RemoteConnector extends Disposable {
888888
return;
889889
}
890890

891-
this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, !!connectionInfo.debugWorkspace, session.accessToken, this.publicApi, this.logger, this.telemetry);
891+
this.heartbeatManager = new HeartbeatManager(connectionInfo.gitpodHost, connectionInfo.workspaceId, connectionInfo.instanceId, !!connectionInfo.debugWorkspace, session, this.publicApi, this.logger, this.telemetry);
892892

893893
try {
894894
// TODO: remove this in the future, gitpod-remote no longer has the heartbeat logic, it's just here until users
@@ -1016,70 +1016,63 @@ export default class RemoteConnector extends Disposable {
10161016
}
10171017
}
10181018

1019-
private async onGitpodRemoteConnection() {
1020-
const remoteUri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0].uri;
1021-
if (!remoteUri) {
1022-
return;
1023-
}
1019+
private async onGitpodRemoteConnection({ remoteAuthority, connectionInfo }: { remoteAuthority: string; connectionInfo: SSHConnectionInfo }) {
1020+
let session: vscode.AuthenticationSession | undefined;
1021+
try {
1022+
session = await this.getGitpodSession(connectionInfo.gitpodHost);
1023+
if (!session) {
1024+
throw new Error('No Gitpod session available');
1025+
}
10241026

1025-
const [, sshDestStr] = remoteUri.authority.split('+');
1026-
const connectionInfo = this.context.globalState.get<SSHConnectionInfo>(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`);
1027-
if (!connectionInfo) {
1028-
return;
1029-
}
1027+
const workspaceInfo = await withServerApi(session.accessToken, connectionInfo.gitpodHost, service => service.server.getWorkspace(connectionInfo.workspaceId), this.logger);
1028+
if (workspaceInfo.latestInstance?.status?.phase !== 'running') {
1029+
throw new NoRunningInstanceError(connectionInfo.workspaceId);
1030+
}
10301031

1031-
const session = await this.getGitpodSession(connectionInfo.gitpodHost);
1032-
if (!session) {
1033-
return;
1034-
}
1032+
if (workspaceInfo.latestInstance.id !== connectionInfo.instanceId) {
1033+
this.logger.info(`Updating workspace ${connectionInfo.workspaceId} latest instance id ${connectionInfo.instanceId} => ${workspaceInfo.latestInstance.id}`);
1034+
connectionInfo.instanceId = workspaceInfo.latestInstance.id;
1035+
}
10351036

1036-
const workspaceInfo = await withServerApi(session.accessToken, connectionInfo.gitpodHost, service => service.server.getWorkspace(connectionInfo.workspaceId), this.logger);
1037-
if (workspaceInfo.latestInstance?.status?.phase !== 'running') {
1038-
return;
1039-
}
1037+
const [, sshDestStr] = remoteAuthority.split('+');
1038+
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo } as SSHConnectionParams);
10401039

1041-
if (workspaceInfo.latestInstance.id !== connectionInfo.instanceId) {
1042-
this.logger.info(`Updating workspace ${connectionInfo.workspaceId} latest instance id ${connectionInfo.instanceId} => ${workspaceInfo.latestInstance.id}`);
1043-
connectionInfo.instanceId = workspaceInfo.latestInstance.id;
1044-
}
1040+
const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost, this.logger);
10451041

1046-
await this.context.globalState.update(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`, { ...connectionInfo, isFirstConnection: false } as SSHConnectionParams);
1042+
await this.initPublicApi(session, connectionInfo.gitpodHost);
10471043

1048-
const gitpodVersion = await getGitpodVersion(connectionInfo.gitpodHost, this.logger);
1044+
if (this.publicApi) {
1045+
this.workspaceState = new WorkspaceState(connectionInfo.workspaceId, this.publicApi, this.logger);
10491046

1050-
await this.initPublicApi(session, connectionInfo.gitpodHost);
1047+
let handled = false;
1048+
this._register(this.workspaceState.onWorkspaceStatusChanged(async () => {
1049+
if (!this.workspaceState!.isWorkspaceRunning() && !handled) {
1050+
handled = true;
1051+
await this.context.globalState.update(`${RemoteConnector.WORKSPACE_STOPPED_PREFIX}${connectionInfo.workspaceId}`, { workspaceId: connectionInfo.workspaceId, gitpodHost: connectionInfo.gitpodHost } as WorkspaceRestartInfo);
1052+
vscode.commands.executeCommand('workbench.action.remote.close');
1053+
}
1054+
}));
1055+
}
10511056

1052-
if (this.publicApi) {
1053-
this.workspaceState = new WorkspaceState(connectionInfo.workspaceId, this.publicApi, this.logger);
1054-
1055-
let handled = false;
1056-
this._register(this.workspaceState.onWorkspaceStatusChanged(async () => {
1057-
if (!this.workspaceState!.isWorkspaceRunning() && !handled) {
1058-
handled = true;
1059-
await this.context.globalState.update(`${RemoteConnector.WORKSPACE_STOPPED_PREFIX}${connectionInfo.workspaceId}`, { workspaceId: connectionInfo.workspaceId, gitpodHost: connectionInfo.gitpodHost } as WorkspaceRestartInfo);
1060-
vscode.commands.executeCommand('workbench.action.remote.close');
1061-
}
1057+
const heartbeatSupported = session.scopes.includes(ScopeFeature.LocalHeartbeat);
1058+
if (heartbeatSupported) {
1059+
this.startHeartBeat(session, connectionInfo);
1060+
} else {
1061+
this.logger.warn(`Local heartbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.raw}`);
1062+
}
1063+
1064+
const syncExtFlow = { ...connectionInfo, gitpodVersion: gitpodVersion.raw, userId: session.account.id, flow: 'sync_local_extensions' };
1065+
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: true, flowId: uuid() });
1066+
this.context.subscriptions.push(vscode.commands.registerCommand('gitpod.installLocalExtensions', () => {
1067+
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: false, flowId: uuid() });
10621068
}));
1063-
}
10641069

1065-
const heartbeatSupported = session.scopes.includes(ScopeFeature.LocalHeartbeat);
1066-
if (heartbeatSupported) {
1067-
this.startHeartBeat(session, connectionInfo);
1068-
} else {
1069-
this.logger.warn(`Local heartbeat not supported in ${connectionInfo.gitpodHost}, using version ${gitpodVersion.raw}`);
1070+
vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', true);
1071+
} catch (e) {
1072+
e.message = `Failed to resolve whole gitpod remote connection process: ${e.message}`;
1073+
this.logger.error(e);
1074+
this.telemetry.sendTelemetryException(e, { workspaceId: connectionInfo.workspaceId, instanceId: connectionInfo.instanceId, userId: session?.account.id || '' });
10701075
}
1071-
1072-
const syncExtFlow = { ...connectionInfo, gitpodVersion: gitpodVersion.raw, userId: session.account.id, flow: 'sync_local_extensions' };
1073-
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: true, flowId: uuid() });
1074-
this.context.subscriptions.push(vscode.commands.registerCommand('gitpod.installLocalExtensions', () => {
1075-
this.initializeRemoteExtensions({ ...syncExtFlow, quiet: false, flowId: uuid() });
1076-
}));
1077-
1078-
this._register(vscode.commands.registerCommand('__gitpod.workspaceShutdown', () => {
1079-
this.logger.warn('__gitpod.workspaceShutdown command executed');
1080-
}));
1081-
1082-
vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', true);
10831076
}
10841077

10851078
private async showWsNotRunningDialog(workspaceId: string, workspaceUrl: string, flow: UserFlowTelemetry) {
@@ -1125,16 +1118,17 @@ export default class RemoteConnector extends Disposable {
11251118
}
11261119
}
11271120

1128-
function isGitpodRemoteWindow(context: vscode.ExtensionContext) {
1121+
function getGitpodRemoteWindow(context: vscode.ExtensionContext): { remoteAuthority: string; connectionInfo: SSHConnectionInfo } | undefined {
11291122
const remoteUri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0].uri;
11301123
if (vscode.env.remoteName === 'ssh-remote' && context.extension.extensionKind === vscode.ExtensionKind.UI && remoteUri) {
11311124
const [, sshDestStr] = remoteUri.authority.split('+');
11321125
const connectionInfo = context.globalState.get<SSHConnectionInfo>(`${RemoteConnector.SSH_DEST_KEY}${sshDestStr}`);
1133-
1134-
return !!connectionInfo;
1126+
if (connectionInfo) {
1127+
return { remoteAuthority: remoteUri.authority, connectionInfo };
1128+
}
11351129
}
11361130

1137-
return false;
1131+
return undefined;
11381132
}
11391133

11401134
function getServiceURL(gitpodHost: string): string {

src/telemetryReporter.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,55 @@ const analyticsClientFactory = async (key: string): Promise<BaseTelemetryClient>
2121
properties: data?.properties
2222
});
2323
} catch (e: any) {
24-
throw new Error('Failed to log event to app analytics!\n' + e.message);
24+
console.error('Failed to log event to app analytics!', e);
2525
}
2626
},
27-
logException: (_exception: Error, _data?: AppenderData) => {
28-
throw new Error('Failed to log exception to app analytics!\n');
27+
logException: (exception: Error, data?: AppenderData) => {
28+
const gitpodHost = vscode.workspace.getConfiguration('gitpod').get<string>('host')!;
29+
const serviceUrl = new URL(gitpodHost);
30+
const errorMetricsEndpoint = `https://ide.${serviceUrl.hostname}/metrics-api/reportError`;
31+
32+
const properties: { [key: string]: any } = Object.assign({}, data?.properties);
33+
properties['error_name'] = exception.name;
34+
properties['error_message'] = exception.message;
35+
properties['debug_workspace'] = String(properties['debug_workspace'] ?? false);
36+
37+
const workspaceId = properties['workspaceId'] ?? '';
38+
const instanceId = properties['instanceId'] ?? '';
39+
const userId = properties['userId'] ?? '';
40+
41+
delete properties['workspaceId'];
42+
delete properties['instanceId'];
43+
delete properties['userId'];
44+
45+
const jsonData = {
46+
component: 'vscode-desktop-extension',
47+
errorStack: exception.stack ?? String(exception),
48+
version: properties['common.extversion'],
49+
workspaceId,
50+
instanceId,
51+
userId,
52+
properties,
53+
};
54+
fetch(errorMetricsEndpoint, {
55+
method: 'POST',
56+
body: JSON.stringify(jsonData),
57+
headers: {
58+
'Content-Type': 'application/json',
59+
},
60+
}).then((resp) => {
61+
if (!resp.ok) {
62+
console.log(`Metrics endpoint responded with ${resp.status} ${resp.statusText}`);
63+
}
64+
}).catch((e) => {
65+
console.error('Failed to report error to metrics endpoint!', e);
66+
});
2967
},
3068
flush: async () => {
3169
try {
3270
await segmentAnalyticsClient.flush();
3371
} catch (e: any) {
34-
throw new Error('Failed to flush app analytics!\n' + e.message);
72+
console.error('Failed to flush app analytics!', e);
3573
}
3674
}
3775
};

0 commit comments

Comments
 (0)