Skip to content

Commit f989e31

Browse files
committed
Report cli command execution error with user's data redacted
This PR fixes #2078. Signed-off-by: Denis Golovin dgolovin@redhat.com
1 parent 53b59cf commit f989e31

File tree

8 files changed

+104
-60
lines changed

8 files changed

+104
-60
lines changed

src/odo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ export class OdoImpl implements Odo {
754754
cwd ? {cwd} : { }
755755
);
756756
if (result.error && fail) {
757-
throw new VsCommandError(`${result.error}`, `Error when running command: ${commandPrivacy}`, result.error);
757+
throw new VsCommandError(`${result.error.message}`, `Error when running command: ${commandPrivacy}`, result.error);
758758
};
759759
return result;
760760
}
@@ -859,7 +859,7 @@ export class OdoImpl implements Odo {
859859

860860
public async createComponentFromFolder(application: OpenShiftObject, type: string, version: string, name: string, location: Uri, starter: string = undefined, useExistingDevfile = false): Promise<OpenShiftObject> {
861861
await this.execute(Command.createLocalComponent(application.getParent().getName(), application.getName(), type, version, name, location.fsPath, starter, useExistingDevfile), location.fsPath);
862-
if (workspace.workspaceFolders && application.getParent().getParent()) { // if there are workspace folders and cluster is acvessible
862+
if (workspace.workspaceFolders && application.getParent().getParent()) { // if there are workspace folders and cluster is accessible
863863
const targetApplication = (await this.getApplications(application.getParent())).find((value) => value === application);
864864
if (!targetApplication) {
865865
await this.insertAndReveal(application);

src/openshift/application.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ export class Application extends OpenShiftItem {
1717
static async describe(treeItem: OpenShiftObject): Promise<void> {
1818
const application = await Application.getOpenShiftCmdData(treeItem,
1919
'Select Application you want to describe');
20-
if (application) Application.odo.executeInTerminal(Command.describeApplication(application.getParent().getName(), application.getName()), undefined, `OpenShift: Describe '${application.getName()}' Application`);
20+
if (application) {
21+
Application.odo.executeInTerminal(
22+
Command.describeApplication(
23+
application.getParent().getName(),
24+
application.getName()
25+
),
26+
undefined,
27+
`OpenShift: Describe '${application.getName()}' Application`
28+
);
29+
}
2130
}
2231

2332
@vsCommand('openshift.app.delete', true)
@@ -29,9 +38,14 @@ export class Application extends OpenShiftItem {
2938
const appName = application.getName();
3039
const value = await window.showWarningMessage(`Do you want to delete Application '${appName}'?`, 'Yes', 'Cancel');
3140
if (value === 'Yes') {
32-
return Progress.execFunctionWithProgress(`Deleting the Application '${appName}'`, () => Application.odo.deleteApplication(application))
33-
.then(() => `Application '${appName}' successfully deleted`)
34-
.catch((err) => Promise.reject(new VsCommandError(`Failed to delete Application with error '${err}'`, 'Failed to delete Application')));
41+
try {
42+
await Progress.execFunctionWithProgress(`Deleting the Application '${appName}'`, () => Application.odo.deleteApplication(application));
43+
return `Application '${appName}' successfully deleted`
44+
} catch (err) {
45+
const telemetryMessage = err instanceof VsCommandError ? err.telemetryMessage : err.message;
46+
throw new VsCommandError(`Failed to delete Application with error '${err.message}'`,
47+
`Failed to delete Application with error ${telemetryMessage}`, err);
48+
}
3549
}
3650
}
3751
return null;

src/openshift/component.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import { SourceType } from '../odo/config';
2727
import { ComponentKind, ComponentTypeAdapter, DevfileComponentType, ImageStreamTag, isDevfileComponent, isImageStreamTag } from '../odo/componentType';
2828
import { Url } from '../odo/url';
2929
import { ComponentDescription, StarterProjectDescription } from '../odo/catalog';
30+
import { isStarterProject, StarterProject } from '../odo/componentTypeDescription';
3031
import path = require('path');
31-
3232
import globby = require('globby');
3333
import treeKill = require('tree-kill');
3434
import fs = require('fs-extra');
35-
import { isStarterProject, StarterProject } from '../odo/componentTypeDescription';
35+
import { NewComponentCommandProps } from '../telemetry';
3636

3737
const waitPort = require('wait-port');
3838

@@ -111,7 +111,7 @@ export class Component extends OpenShiftItem {
111111
static async create(application: OpenShiftApplication): Promise<string> {
112112
if (!application) return null;
113113

114-
return Component.createFromLocal(application).catch((err) => Promise.reject(new VsCommandError(`Failed to create Component with error '${err}'`, 'Failed to create Component with error')));
114+
return Component.createFromLocal(application);
115115
}
116116

117117
@vsCommand('openshift.component.delete', true)
@@ -684,34 +684,45 @@ export class Component extends OpenShiftItem {
684684
}
685685

686686
const refreshComponentsView = workspace.getWorkspaceFolder(folder);
687-
688-
await Progress.execFunctionWithProgress(
689-
`Creating new Component '${componentName}'`,
690-
() => Component.odo.createComponentFromFolder(
691-
application,
692-
componentType? componentType.name : undefined, // in case of using existing devfile
693-
componentType? componentType.version : undefined,
694-
componentName,
695-
folder,
696-
createStarter,
697-
useExistingDevfile
698-
)
699-
);
700-
701-
// when creating component based on existing workspace folder refresh components view
702-
if (refreshComponentsView) {
703-
commands.executeCommand('openshift.componentsView.refresh');
704-
}
705-
706-
const result:any = new String(`Component '${componentName}' successfully created. To deploy it on cluster, perform 'Push' action.`);
707-
result.properties = {
687+
const creatComponentProperties: NewComponentCommandProps = {
708688
'component_kind': componentType?.version ? ComponentKind.S2I: ComponentKind.DEVFILE,
709689
'component_type': componentType?.name,
710690
'component_version': componentType?.version,
711691
'starter_project': createStarter,
712692
'use_existing_devfile': useExistingDevfile,
713693
};
714-
return result;
694+
try {
695+
await Progress.execFunctionWithProgress(
696+
`Creating new Component '${componentName}'`,
697+
() => Component.odo.createComponentFromFolder(
698+
application,
699+
componentType? componentType.name : undefined, // in case of using existing devfile
700+
componentType? componentType.version : undefined,
701+
componentName,
702+
folder,
703+
createStarter,
704+
useExistingDevfile
705+
)
706+
);
707+
708+
// when creating component based on existing workspace folder refresh components view
709+
if (refreshComponentsView) {
710+
commands.executeCommand('openshift.componentsView.refresh');
711+
}
712+
713+
const result:any = new String(`Component '${componentName}' successfully created. To deploy it on cluster, perform 'Push' action.`);
714+
result.properties = creatComponentProperties;
715+
return result;
716+
} catch (err) {
717+
if (err instanceof VsCommandError) {
718+
throw new VsCommandError(
719+
`Error occurred while creating Component '${componentName}': ${err.message}`,
720+
`Error occurred while creating Component: ${err.telemetryMessage}`, err,
721+
creatComponentProperties
722+
);
723+
}
724+
throw err;
725+
}
715726
}
716727

717728
@vsCommand('openshift.component.createFromGit')

src/telemetry.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,22 @@ export default async function sendTelemetry(actionName: string, properties?: any
2626
return service?.sendStartupEvent();
2727
}
2828
return service?.send(createTrackingEvent(actionName, properties));
29-
}
29+
}
30+
31+
export interface CommonCommandProps {
32+
identifier: string;
33+
error: string;
34+
'stack_trace': string;
35+
duration: number;
36+
cancelled: boolean;
37+
}
38+
39+
export interface NewComponentCommandProps {
40+
'component_kind': string;
41+
'component_type': string;
42+
'component_version': string;
43+
'starter_project': string;
44+
'use_existing_devfile': boolean;
45+
}
46+
47+
export type AllProps = Partial<CommonCommandProps & NewComponentCommandProps>;

src/vscommand.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
*-----------------------------------------------------------------------------------------------*/
55

66
/* eslint-disable @typescript-eslint/no-explicit-any */
7+
/* eslint-disable camelcase */
8+
/* eslint-disable @typescript-eslint/camelcase */
79

810
import { commands, Disposable, window } from 'vscode';
911
import * as stackTraceParser from 'stacktrace-parser';
10-
import sendTelemetry from './telemetry';
12+
import sendTelemetry, { AllProps, CommonCommandProps } from './telemetry';
1113
import { ExtenisonID } from './util/constants';
1214

1315
type VsCommandFunction = (...args: any[]) => Promise<any> | any;
@@ -18,8 +20,13 @@ interface VsCommand {
1820
method: VsCommandFunction;
1921
}
2022

23+
export interface Result<ReturnType> {
24+
value: ReturnType;
25+
properties: AllProps;
26+
}
27+
2128
export class VsCommandError extends Error {
22-
constructor(message: string, public readonly telemetryMessage = message, public parent?: Error) {
29+
constructor(message: string, public readonly telemetryMessage = message, public readonly parent?, public readonly telemetryProps: AllProps = {}) {
2330
super(message);
2431
Object.setPrototypeOf(this, new.target.prototype);
2532
}
@@ -40,52 +47,50 @@ export async function registerCommands(...modules: string[]): Promise<Disposable
4047
await Promise.all(modules.map((module) => import(module)));
4148
return vsCommands.map((cmd) => {
4249
return commands.registerCommand(cmd.commandId, async (...params) => {
43-
let telemetryProps: any = {
50+
let telemetryProps: Partial<CommonCommandProps> = {
4451
identifier: cmd.commandId,
4552
};
4653
let result: any;
54+
let exception: any;
4755
const startTime = Date.now();
48-
let stackTrace = {};
4956
try {
5057
result = await Promise.resolve(cmd.method.call(null, ...params));
5158
displayResult(result);
5259
} catch (err) {
60+
let stack:stackTraceParser.StackFrame[];
5361
if (err.stack) {
54-
const stack = stackTraceParser.parse(err.stack); // TODO: add recursive stacktrace parsing for parent errors
62+
stack = stackTraceParser.parse(err.stack); // TODO: add recursive stacktrace parsing for parent errors
5563
if (stack.length > 0) {
5664
const files = stack.map((value) => `${value.file.substring(value.file.lastIndexOf(ExtenisonID)-1)}:${value.lineNumber}:${value.column}`);
57-
stackTrace = {
58-
'stack_trace': files.join('\n')
59-
};
65+
telemetryProps.stack_trace = files.join('\n')
6066
}
6167
}
6268
if (err instanceof VsCommandError) {
6369
// exception thrown by extension command with meaningful message
6470
// just show it and return
6571
telemetryProps.error = err.telemetryMessage;
66-
window.showErrorMessage(err.message);
6772
} else {
68-
// Unexpected exception happened. Let vscode handle the error reporting.
69-
// This does not work when command started by pressing button in view title
70-
// TODO: Wrap view title commands in try/catch and re-throw as VsCommandError
71-
// TODO: telemetry cannot send not known exception stacktrace or message
72-
// because it can contain user's sensitive information
73+
// Unexpected exception happened.
7374
if (err instanceof TypeError) {
7475
telemetryProps.error = err.message;
7576
} else {
7677
telemetryProps.error = 'Unexpected error';
7778
}
78-
window.showErrorMessage(err.toString());
7979
}
80+
window.showErrorMessage(err.message);
81+
exception = err;
8082
} finally {
8183
telemetryProps.duration = Date.now() - startTime;
8284
telemetryProps.cancelled = result === null;
8385
if (result?.properties) {
8486
telemetryProps = {...telemetryProps, ...result.properties};
8587
}
86-
telemetryProps = {...telemetryProps, ...stackTrace };
88+
if (exception?.telemetryProps) {
89+
telemetryProps = {...telemetryProps, ...exception.telemetryProps};
90+
}
8791
sendTelemetry('command', telemetryProps);
8892
}
93+
return result;
8994
});
9095
});
9196
}

test/unit/extension.test.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,9 @@ suite('openshift connector Extension', () => {
156156
sandbox.stub(vscode.window, 'showWarningMessage').resolves('Yes');
157157
sandbox.stub(vscode.window, 'showInformationMessage');
158158
const semStub = sandbox.stub(vscode.window, 'showErrorMessage');
159-
sandbox.stub(Progress, 'execFunctionWithProgress').rejects(Error('message'));
160-
let error;
161-
try {
162-
await vscode.commands.executeCommand('openshift.app.delete', appItem);
163-
} catch (err) {
164-
error = err;
165-
}
166-
expect(error).not.null;
167-
expect(semStub).calledWith('Failed to delete Application with error \'Error: message\'');
159+
const error = new Error('message');
160+
sandbox.stub(Progress, 'execFunctionWithProgress').rejects(error);
161+
await vscode.commands.executeCommand('openshift.app.delete', appItem);
162+
expect(semStub).calledWith(`Failed to delete Application with error '${error.message}'`);
168163
});
169164
});

test/unit/openshift/application.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Command } from '../../../src/odo/command';
1212
import { Application } from '../../../src/openshift/application';
1313
import { TestItem } from './testOSItem';
1414
import OpenShiftItem from '../../../src/openshift/openshiftItem';
15+
import { VsCommandError } from '../../../src/vscommand';
1516

1617
const {expect} = chai;
1718
chai.use(sinonChai);
@@ -125,7 +126,7 @@ suite('OpenShift/Application', () => {
125126

126127
test('throws an error message when odo command failed', async () => {
127128
warnStub.resolves('Yes');
128-
execStub.rejects('ERROR');
129+
execStub.rejects(new VsCommandError('ERROR', 'ERROR'));
129130
let expectedError;
130131
try {
131132
await Application.del(appItem);

test/unit/openshift/component.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,14 @@ suite('OpenShift/Component', () => {
125125
});
126126

127127
test('errors when a subcommand fails', async () => {
128-
quickPickStub.onFirstCall().rejects(errorMessage);
128+
quickPickStub.onFirstCall().rejects(new Error(errorMessage));
129129
let expectedError: Error;
130130
try {
131131
await Component.create(appItem);
132132
} catch (error) {
133133
expectedError = error;
134134
}
135-
expect(expectedError.message).equals(`Failed to create Component with error '${errorMessage}'`);
135+
expect(expectedError.message).equals(errorMessage);
136136
});
137137

138138
suite('from local workspace', () => {

0 commit comments

Comments
 (0)