Skip to content

Commit

Permalink
Add UnhandledError telemetry error report
Browse files Browse the repository at this point in the history
  • Loading branch information
mustard-mh committed Sep 23, 2022
1 parent 9ae3127 commit 139f4a4
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 45 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
},
"dependencies": {
"@gitpod/gitpod-protocol": "main",
"@gitpod/ide-metrics-api-grpcweb": "ak-ext-metrics",
"@gitpod/ide-metrics-api-grpcweb": "^0.0.1-main.4780",
"@gitpod/local-app-api-grpcweb": "main",
"@gitpod/supervisor-api-grpc": "main",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
Expand Down
2 changes: 1 addition & 1 deletion remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@gitpod/gitpod-protocol": "main",
"@gitpod/ide-metrics-api-grpcweb": "ak-ext-metrics",
"@gitpod/ide-metrics-api-grpcweb": "^0.0.1-main.4783",
"@gitpod/supervisor-api-grpc": "main",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@microsoft/1ds-core-js": "^3.2.2",
Expand Down
2 changes: 1 addition & 1 deletion remote/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@gitpod/ide-metrics-api-grpcweb": "^0.0.1-main.4783",
"@gitpod/local-app-api-grpcweb": "main",
"@gitpod/ide-metrics-api-grpcweb": "ak-ext-metrics",
"@microsoft/1ds-core-js": "^3.2.2",
"@microsoft/1ds-post-js": "^3.2.2",
"@vscode/iconv-lite-umd": "0.7.0",
Expand Down
8 changes: 4 additions & 4 deletions remote/web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# yarn lockfile v1


"@gitpod/ide-metrics-api-grpcweb@ak-ext-metrics":
version "0.0.1-ak-ext-metrics.4"
resolved "https://registry.yarnpkg.com/@gitpod/ide-metrics-api-grpcweb/-/ide-metrics-api-grpcweb-0.0.1-ak-ext-metrics.4.tgz#9dacee7f13181e132fba9e4a5b97cb7f4b1739d2"
integrity sha512-s1C4W5Q7nlgyaQGCKqG8gI1ZdzwsaFZW2k8VX5hJKIcpD5S60I7AJbP1wVxYhRqR93YW3++DHydSpbjBUVnQFA==
"@gitpod/ide-metrics-api-grpcweb@^0.0.1-main.4783":
version "0.0.1-main.4783"
resolved "https://registry.yarnpkg.com/@gitpod/ide-metrics-api-grpcweb/-/ide-metrics-api-grpcweb-0.0.1-main.4783.tgz#d618c0d37713b66eec3a58ae9fc76c91ace222c9"
integrity sha512-QxQT/chiDd7qRFUWNhsTHPBi6dgX9n1nuqHpUmqaiXq8KSWHGss4XY2jUfY+V64zmIQmKp/B/M+1H7VWIOQcUg==
dependencies:
"@improbable-eng/grpc-web" "^0.14.0"
google-protobuf "^3.19.1"
Expand Down
8 changes: 4 additions & 4 deletions remote/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
vscode-ws-jsonrpc "^0.2.0"
ws "^7.4.6"

"@gitpod/ide-metrics-api-grpcweb@ak-ext-metrics":
version "0.0.1-ak-ext-metrics.4"
resolved "https://registry.yarnpkg.com/@gitpod/ide-metrics-api-grpcweb/-/ide-metrics-api-grpcweb-0.0.1-ak-ext-metrics.4.tgz#9dacee7f13181e132fba9e4a5b97cb7f4b1739d2"
integrity sha512-s1C4W5Q7nlgyaQGCKqG8gI1ZdzwsaFZW2k8VX5hJKIcpD5S60I7AJbP1wVxYhRqR93YW3++DHydSpbjBUVnQFA==
"@gitpod/ide-metrics-api-grpcweb@^0.0.1-main.4783":
version "0.0.1-main.4783"
resolved "https://registry.yarnpkg.com/@gitpod/ide-metrics-api-grpcweb/-/ide-metrics-api-grpcweb-0.0.1-main.4783.tgz#d618c0d37713b66eec3a58ae9fc76c91ace222c9"
integrity sha512-QxQT/chiDd7qRFUWNhsTHPBi6dgX9n1nuqHpUmqaiXq8KSWHGss4XY2jUfY+V64zmIQmKp/B/M+1H7VWIOQcUg==
dependencies:
"@improbable-eng/grpc-web" "^0.14.0"
google-protobuf "^3.19.1"
Expand Down
1 change: 1 addition & 0 deletions src/vs/base/common/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export interface IGitpodPreviewConfiguration {
log?: {
analytics?: boolean;
metrics?: boolean;
errorReports?: boolean;
};
}

Expand Down
106 changes: 89 additions & 17 deletions src/vs/gitpod/browser/gitpodInsightsAppender.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
/* eslint-disable code-import-patterns */
/* eslint-disable local/code-import-patterns */
/* eslint-disable header/header */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Gitpod. All rights reserved.
*--------------------------------------------------------------------------------------------*/

/// <reference types='@gitpod/gitpod-protocol/lib/typings/globals'/>

import { IProductService } from 'vs/platform/product/common/productService';
import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { mapMetrics, mapTelemetryData } from 'vs/gitpod/common/insightsHelper';
import { mapMetrics, mapTelemetryData, ReportErrorParam } from 'vs/gitpod/common/insightsHelper';
import type { IDEMetric } from '@gitpod/ide-metrics-api-grpcweb';
import type { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry';

type SendMetrics = (metrics: IDEMetric[]) => Promise<void>;
type ErrorReports = (errors: ReportErrorParam) => Promise<void>;
interface SupervisorWorkspaceInfo { gitpodHost: string; instanceId: string; workspaceId: string }

export class GitpodInsightsAppender implements ITelemetryAppender {
private readonly _baseProperties: { appName: string; uiKind: 'web'; version: string };
private readonly devMode = this.productService.nameShort.endsWith(' Dev');
private gitpodUserId: string | undefined;
constructor(
@IProductService private readonly productService: IProductService
) {
Expand All @@ -22,11 +28,19 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
uiKind: 'web',
version: this.productService.version,
};
window.gitpod?.service.server.getLoggedInUser().then((user) => {
this.gitpodUserId = user.id;
}).catch((e) => {
console.error('failed to get gitpodUserId', e);
});
}

public log(eventName: string, data: any): void {
this.sendAnalytics(eventName, data);
this.sendMetrics(eventName, data);
if (eventName === 'UnhandledError') {
this.sendErrorReports(data as ErrorEvent);
}
}

private sendAnalytics(eventName: string, data: any): void {
Expand Down Expand Up @@ -81,33 +95,91 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
return this._sendMetrics;
}
return this._sendMetrics = (async () => {
let gitpodHost: string | undefined;
if (!this.devMode) {
const infoResponse = await fetch(window.location.protocol + '//' + window.location.host + '/_supervisor/v1/info/workspace', {
credentials: 'include'
});
if (!infoResponse.ok) {
throw new Error(`Getting workspace info failed: ${infoResponse.statusText}`);
}
const info: { gitpodHost: string } = await infoResponse.json();
gitpodHost = new URL(info.gitpodHost).host;
} else if (this.productService.gitpodPreview) {
gitpodHost = this.productService.gitpodPreview.host;
}
if (!gitpodHost) {
const gitpodWsInfo = await this.getGitpodWorkspaceInfo();
if (!gitpodWsInfo.gitpodHost) {
return undefined;
}
// load grpc-web before see https://github.com/gitpod-io/gitpod/issues/4448
await import('@improbable-eng/grpc-web');
const { MetricsServiceClient, sendMetrics } = await import('@gitpod/ide-metrics-api-grpcweb');
const ideMetricsEndpoint = 'https://ide.' + gitpodHost + '/metrics-api';
const ideMetricsEndpoint = 'https://ide.' + gitpodWsInfo.gitpodHost + '/metrics-api';
const client = new MetricsServiceClient(ideMetricsEndpoint);
return async (metrics: IDEMetric[]) => {
await sendMetrics(client, metrics);
};
})();
}

private async sendErrorReports(error: ErrorEvent) {
const gitpodWsInfo = await this.getGitpodWorkspaceInfo();
const params: ReportErrorParam = {
workspaceId: gitpodWsInfo.workspaceId,
instanceId: gitpodWsInfo.instanceId,
errorStack: error.callstack,
userId: this.gitpodUserId ?? '',
component: 'vscode-web',
version: this._baseProperties.version,
properties: {
error_name: error.uncaught_error_name,
error_message: error.msg,
...this._baseProperties,
}
};
if (this.devMode && this.productService.gitpodPreview?.log?.errorReports) {
console.log('Gitpod Error Reports: ', JSON.stringify(params, undefined, 2));
}
const doSend = await this.getSendErrorReports();
if (doSend) {
await doSend(params);
}
}

private _sendErrorReports: Promise<ErrorReports | undefined> | undefined;
private getSendErrorReports(): Promise<ErrorReports | undefined> {
if (this._sendErrorReports) {
return this._sendErrorReports;
}
return this._sendErrorReports = (async () => {
const gitpodWsInfo = await this.getGitpodWorkspaceInfo();
if (!gitpodWsInfo.gitpodHost) {
return undefined;
}
const ideMetricsHttpEndpoint = 'https://ide.' + gitpodWsInfo.gitpodHost + '/metrics-api/reportError';
return async (params: ReportErrorParam) => {
const response = await fetch(ideMetricsHttpEndpoint, {
method: 'POST',
body: JSON.stringify(params),
credentials: 'omit',
});
if (!response.ok) {
const data = await response.json();
console.error(`Cannot report error: ${response.status} ${response.statusText}`, data);
}
};
})();
}

private _gitpodWsInfo: Promise<SupervisorWorkspaceInfo> | undefined;
private getGitpodWorkspaceInfo(): Promise<SupervisorWorkspaceInfo> {
if (this._gitpodWsInfo) {
return this._gitpodWsInfo;
}
return this._gitpodWsInfo = (async () => {
const infoResponse = await fetch(window.location.protocol + '//' + window.location.host + '/_supervisor/v1/info/workspace', {
credentials: 'include'
});
if (!infoResponse.ok) {
throw new Error(`Getting workspace info failed: ${infoResponse.statusText}`);
}
const info: SupervisorWorkspaceInfo = await infoResponse.json();
return {
gitpodHost: this.devMode ? this.productService.gitpodPreview?.host ?? 'gitpod-staging.com' : new URL(info.gitpodHost).host,
instanceId: info.instanceId,
workspaceId: info.workspaceId
};
})();
}

public flush(): Promise<any> {
return Promise.resolve(undefined);
}
Expand Down
24 changes: 16 additions & 8 deletions src/vs/gitpod/common/insightsHelper.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
/* eslint-disable code-import-patterns */
/* eslint-disable local/code-import-patterns */
/* eslint-disable header/header */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Gitpod. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import { RemoteTrackMessage } from '@gitpod/gitpod-protocol/lib/analytics';
import type { IDEMetric } from '@gitpod/ide-metrics-api-grpcweb/lib/index';
import type { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry';

export interface GitpodErrorEvent extends ErrorEvent {
fromBrowser?: boolean;
}

export interface ReportErrorParam {
workspaceId: string;
instanceId: string;
errorStack: string;
userId: string;
component: string;
version: string;
properties?: Record<string, any>;
}

function getEventName(name: string) {
const str = name.replace('remoteConnection', '').replace('remoteReconnection', '');
Expand All @@ -23,17 +37,11 @@ function getEventName(name: string) {
let readAccessTracked = false;
let writeAccessTracked = false;

export enum SenderKind {
Browser = 1,
Node = 2
}

// TODO map 'UnhandledError' to our error and report it both only for window and remote-server

export function mapMetrics(source: 'window' | 'remote-server', eventName: string, data: any): IDEMetric[] | undefined {
const maybeMetrics = doMapMetrics(source, eventName, data);
return maybeMetrics instanceof Array ? maybeMetrics : typeof maybeMetrics === 'object' ? [maybeMetrics] : undefined;
}

function doMapMetrics(source: 'window' | 'remote-server', eventName: string, data: any): IDEMetric[] | IDEMetric | undefined {
if (source === 'remote-server') {
if (eventName.startsWith('extensionGallery:')) {
Expand Down
49 changes: 45 additions & 4 deletions src/vs/gitpod/node/gitpodInsightsAppender.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable code-import-patterns */
/* eslint-disable local/code-import-patterns */
/* eslint-disable header/header */
/*---------------------------------------------------------------------------------------------
* Copyright (c) Gitpod. All rights reserved.
Expand All @@ -21,9 +21,10 @@ import * as grpc from '@grpc/grpc-js';
import * as util from 'util';
import { filter, mixin } from 'vs/base/common/objects';
import { mapMetrics, mapTelemetryData } from 'vs/gitpod/common/insightsHelper';
import { MetricsServiceClient, sendMetrics } from '@gitpod/ide-metrics-api-grpcweb';
import { MetricsServiceClient, sendMetrics, ReportErrorRequest } from '@gitpod/ide-metrics-api-grpcweb';
import { IGitpodPreviewConfiguration } from 'vs/base/common/product';
import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport';
import type { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry';

class SupervisorConnection {
readonly deadlines = {
Expand All @@ -45,7 +46,7 @@ class SupervisorConnection {
}

type GitpodConnection = Omit<GitpodServiceImpl<GitpodClient, GitpodServer>, 'server'> & {
server: Pick<GitpodServer, 'trackEvent'>;
server: Pick<GitpodServer, 'trackEvent' | 'getLoggedInUser'>;
};

export class GitpodInsightsAppender implements ITelemetryAppender {
Expand All @@ -55,6 +56,7 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
private _baseProperties: { appName: string; uiKind: 'web'; version: string };
private readonly supervisor = new SupervisorConnection();
private readonly devMode = this.productName.endsWith(' Dev');
private gitpodUserId: string | undefined;

constructor(private productName: string, private productVersion: string, private readonly gitpodPreview?: IGitpodPreviewConfiguration) {
this._asyncAIClient = null;
Expand All @@ -63,9 +65,12 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
uiKind: 'web',
version: productVersion,
};
this._withAIClient(async (client) => {
this.gitpodUserId = (await client.getLoggedInUser()).id;
});
}

private _withAIClient(callback: (aiClient: Pick<GitpodServer, 'trackEvent'>) => void): void {
private _withAIClient(callback: (aiClient: Pick<GitpodServer, 'trackEvent' | 'getLoggedInUser'>) => void): void {
if (!this._asyncAIClient) {
this._asyncAIClient = this.getSupervisorData().then(
(supervisorData) => {
Expand Down Expand Up @@ -93,6 +98,12 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
log(eventName: string, data?: any): void {
this.sendAnalytics(data, eventName);
this.sendMetrics(data, eventName);
if (eventName === 'UnhandledError') {
if (data.fromBrowser) {
return;
}
this.sendErrorReport(data as ErrorEvent);
}
}

private async sendAnalytics(data: any, eventName: string): Promise<void> {
Expand Down Expand Up @@ -142,6 +153,35 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
}
}

private async sendErrorReport(error: ErrorEvent): Promise<void> {
const req = new ReportErrorRequest();
req.setWorkspaceId(this._defaultData['workspaceId']);
req.setInstanceId(this._defaultData['instanceId']);
req.setErrorStack(error.callstack);
if (this.gitpodUserId) {
req.setUserId(this.gitpodUserId);
}
req.setComponent('vscode-server');
req.setVersion(this.productVersion);
req.getPropertiesMap().set('error_name', error.uncaught_error_name || '');
req.getPropertiesMap().set('error_message', error.msg || '');
req.getPropertiesMap().set('appName', this._baseProperties.appName);
req.getPropertiesMap().set('uiKind', this._baseProperties.uiKind);
req.getPropertiesMap().set('version', this._baseProperties.version);

if (this.devMode) {
console.log('Gitpod Error Reports: ', JSON.stringify(req.toObject(), null, 2));
}
const client = await this.getMetricsClient();
if (client) {
client.reportError(req, (e) => {
if (e) {
console.error('failed to send IDE error report:', e);
}
});
}
}

flush(): Promise<any> {
return Promise.resolve(undefined);
}
Expand Down Expand Up @@ -211,6 +251,7 @@ export class GitpodInsightsAppender implements ITelemetryAppender {
getTokenRequest.setKind('gitpod');
getTokenRequest.setHost(gitpodApiHost);
getTokenRequest.addScope('function:trackEvent');
getTokenRequest.addScope('function:getLoggedInUser');


const supervisor = this.supervisor;
Expand Down
Loading

0 comments on commit 139f4a4

Please sign in to comment.