Skip to content

Commit 5618e09

Browse files
authored
NEW @W-17915999@ Added telemetry collection for run action (#1778)
1 parent c90f41a commit 5618e09

File tree

11 files changed

+189
-15500
lines changed

11 files changed

+189
-15500
lines changed

package-lock.json

Lines changed: 0 additions & 15492 deletions
This file was deleted.

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
"bugs": "https://github.com/forcedotcom/sfdx-scanner/issues",
77
"dependencies": {
88
"@oclif/core": "3.27.0",
9-
"@salesforce/code-analyzer-core": "0.26.0",
10-
"@salesforce/code-analyzer-engine-api": "0.21.0",
11-
"@salesforce/code-analyzer-eslint-engine": "0.21.0",
12-
"@salesforce/code-analyzer-flow-engine": "0.19.0",
13-
"@salesforce/code-analyzer-pmd-engine": "0.22.0",
14-
"@salesforce/code-analyzer-regex-engine": "0.19.0",
15-
"@salesforce/code-analyzer-retirejs-engine": "0.19.0",
16-
"@salesforce/code-analyzer-sfge-engine": "0.2.0",
9+
"@salesforce/code-analyzer-core": "0.27.0",
10+
"@salesforce/code-analyzer-engine-api": "0.22.0",
11+
"@salesforce/code-analyzer-eslint-engine": "0.22.0",
12+
"@salesforce/code-analyzer-flow-engine": "0.20.0",
13+
"@salesforce/code-analyzer-pmd-engine": "0.23.0",
14+
"@salesforce/code-analyzer-regex-engine": "0.20.0",
15+
"@salesforce/code-analyzer-retirejs-engine": "0.20.0",
16+
"@salesforce/code-analyzer-sfge-engine": "0.3.1",
1717
"@salesforce/core": "6.7.6",
1818
"@salesforce/sf-plugins-core": "5.0.13",
1919
"@salesforce/ts-types": "^2.0.12",

src/Constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@ export enum View {
33
DETAIL = 'detail',
44
TABLE = 'table'
55
}
6+
7+
export const TelemetryEventName = 'plugin-code-analyzer';
8+
9+
export const CliTelemetryEvents = {
10+
ENGINE_SELECTION: 'engine_selection',
11+
ENGINE_EXECUTION: 'engine_execution'
12+
}

src/commands/code-analyzer/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {BundleName, getMessage, getMessages} from '../../lib/messages';
1111
import {LogEventDisplayer} from '../../lib/listeners/LogEventListener';
1212
import {EngineRunProgressSpinner, RuleSelectionProgressSpinner} from '../../lib/listeners/ProgressEventListener';
1313
import {Displayable, UxDisplay} from '../../lib/Display';
14+
import {SfCliTelemetryEmitter} from "../../lib/Telemetry";
1415

1516
export default class RunCommand extends SfCommand<void> implements Displayable {
1617
// We don't need the `--json` output for this command.
@@ -96,6 +97,7 @@ export default class RunCommand extends SfCommand<void> implements Displayable {
9697
configFactory: new CodeAnalyzerConfigFactoryImpl(),
9798
pluginsFactory: new EnginePluginsFactoryImpl(),
9899
writer: CompositeResultsWriter.fromFiles(outputFiles),
100+
telemetryEmitter: new SfCliTelemetryEmitter(),
99101
logEventListeners: [new LogEventDisplayer(uxDisplay)],
100102
progressListeners: [new EngineRunProgressSpinner(uxDisplay), new RuleSelectionProgressSpinner(uxDisplay)],
101103
resultsViewer,

src/lib/Telemetry.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {TelemetryData} from '@salesforce/code-analyzer-core';
2+
import {Lifecycle} from "@salesforce/core";
3+
4+
export interface TelemetryEmitter {
5+
emitTelemetry(source: string, eventName: string, data: TelemetryData): void;
6+
}
7+
8+
export class SfCliTelemetryEmitter implements TelemetryEmitter {
9+
// istanbul ignore next - No sense in covering SF-CLI core code.
10+
public emitTelemetry(source: string, eventName: string, data: TelemetryData): void {
11+
return void Lifecycle.getInstance().emitTelemetry({
12+
...data,
13+
source,
14+
eventName
15+
});
16+
}
17+
}

src/lib/actions/RunAction.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ import {LogFileWriter} from '../writers/LogWriter';
1818
import {LogEventListener, LogEventLogger} from '../listeners/LogEventListener';
1919
import {ProgressEventListener} from '../listeners/ProgressEventListener';
2020
import {BundleName, getMessage} from '../messages';
21+
import {TelemetryEmitter} from "../Telemetry";
22+
import {TelemetryEventListener} from "../listeners/TelemetryEventListener";
23+
import * as Constants from '../../Constants';
2124

2225
export type RunDependencies = {
2326
configFactory: CodeAnalyzerConfigFactory;
2427
pluginsFactory: EnginePluginsFactory;
2528
logEventListeners: LogEventListener[];
2629
progressListeners: ProgressEventListener[];
30+
telemetryEmitter: TelemetryEmitter;
2731
writer: ResultsWriter;
2832
resultsViewer: ResultsViewer;
2933
actionSummaryViewer: RunActionSummaryViewer;
@@ -56,6 +60,8 @@ export class RunAction {
5660
// LogEventListeners should start listening as soon as the Core is instantiated, since Core can start emitting
5761
// events they listen for basically immediately.
5862
this.dependencies.logEventListeners.forEach(listener => listener.listen(core));
63+
const telemetryListener: TelemetryEventListener = new TelemetryEventListener(this.dependencies.telemetryEmitter);
64+
telemetryListener.listen(core);
5965
const enginePlugins = this.dependencies.pluginsFactory.create();
6066
const enginePluginModules = config.getCustomEnginePluginModules();
6167
const addEnginePromises: Promise<void>[] = [
@@ -71,10 +77,12 @@ export class RunAction {
7177
const ruleSelection: RuleSelection = await core.selectRules(input['rule-selector'], {workspace});
7278
const runOptions: RunOptions = {workspace};
7379
const results: RunResults = await core.run(ruleSelection, runOptions);
80+
this.emitEngineTelemetry(ruleSelection, results, enginePlugins.flatMap(p => p.getAvailableEngineNames()));
7481
// After Core is done running, the listeners need to be told to stop, since some of them have persistent UI elements
7582
// or file handlers that must be gracefully ended.
7683
this.dependencies.progressListeners.forEach(listener => listener.stopListening());
7784
this.dependencies.logEventListeners.forEach(listener => listener.stopListening());
85+
telemetryListener.stopListening();
7886
this.dependencies.writer.write(results);
7987
this.dependencies.resultsViewer.view(results);
8088
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(results, logWriter.getLogDestination(), input['output-file']);
@@ -88,6 +96,26 @@ export class RunAction {
8896
public static createAction(dependencies: RunDependencies): RunAction {
8997
return new RunAction(dependencies);
9098
}
99+
100+
private emitEngineTelemetry(ruleSelection: RuleSelection, results: RunResults, coreEngineNames: string[]): void {
101+
const selectedEngineNames: Set<string> = new Set(ruleSelection.getEngineNames());
102+
for (const coreEngineName of coreEngineNames) {
103+
if (!selectedEngineNames.has(coreEngineName)) {
104+
continue;
105+
}
106+
this.dependencies.telemetryEmitter.emitTelemetry('RunAction', Constants.TelemetryEventName, {
107+
sfcaEvent: Constants.CliTelemetryEvents.ENGINE_SELECTION,
108+
engine: coreEngineName,
109+
ruleCount: ruleSelection.getRulesFor(coreEngineName).length
110+
});
111+
112+
this.dependencies.telemetryEmitter.emitTelemetry('RunAction', Constants.TelemetryEventName, {
113+
sfcaEvent: Constants.CliTelemetryEvents.ENGINE_EXECUTION,
114+
engine: coreEngineName,
115+
violationCount: results.getEngineRunResults(coreEngineName).getViolationCount()
116+
});
117+
}
118+
}
91119
}
92120

93121
function throwErrorIfSevThresholdExceeded(threshold: SeverityLevel, results: RunResults): void {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
CodeAnalyzer,
3+
EngineTelemetryEvent,
4+
EventType,
5+
TelemetryEvent
6+
} from "@salesforce/code-analyzer-core";
7+
import {TelemetryEmitter} from '../Telemetry';
8+
import * as constants from '../../Constants';
9+
10+
export class TelemetryEventListener {
11+
private telemetryEmitter: TelemetryEmitter;
12+
13+
public constructor(telemetryEmitter: TelemetryEmitter) {
14+
this.telemetryEmitter = telemetryEmitter;
15+
}
16+
17+
public listen(codeAnalyzer: CodeAnalyzer): void {
18+
// Set up listeners.
19+
codeAnalyzer.onEvent(EventType.TelemetryEvent, (e: TelemetryEvent) => this.handleEvent('Core', e));
20+
codeAnalyzer.onEvent(EventType.EngineTelemetryEvent, (e: EngineTelemetryEvent) => this.handleEvent(e.engineName, e));
21+
}
22+
23+
public stopListening(): void {
24+
// Intentional no-op, because no cleanup is required.
25+
}
26+
27+
private handleEvent(source: string, event: TelemetryEvent|EngineTelemetryEvent): void {
28+
return this.telemetryEmitter.emitTelemetry(source, constants.TelemetryEventName, {
29+
...event.data,
30+
sfcaEvent: event.eventName,
31+
timestamp: event.timestamp.getTime(),
32+
uuid: event.uuid
33+
});
34+
}
35+
}

test/commands/code-analyzer/run.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import {stubSfCommandUx} from '@salesforce/sf-plugins-core';
2+
import {TelemetryData} from '@salesforce/code-analyzer-core';
23
import {TestContext} from '@salesforce/core/lib/testSetup';
34
import RunCommand from '../../../src/commands/code-analyzer/run';
45
import {RunAction, RunDependencies, RunInput} from '../../../src/lib/actions/RunAction';
56
import {CompositeResultsWriter} from '../../../src/lib/writers/ResultsWriter';
7+
import {SfCliTelemetryEmitter} from "../../../src/lib/Telemetry";
8+
9+
type TelemetryEmission = {
10+
source: string,
11+
eventName: string,
12+
data: TelemetryData
13+
};
614

715
describe('`code-analyzer run` tests', () => {
816
const $$ = new TestContext();
917

1018
let executeSpy: jest.SpyInstance;
1119
let createActionSpy: jest.SpyInstance;
20+
let receivedTelemetryEmissions: TelemetryEmission[];
1221
let receivedActionInput: RunInput;
1322
let receivedActionDependencies: RunDependencies;
1423
let fromFilesSpy: jest.SpyInstance;
@@ -19,6 +28,11 @@ describe('`code-analyzer run` tests', () => {
1928
receivedActionInput = input;
2029
return Promise.resolve();
2130
});
31+
receivedTelemetryEmissions = [];
32+
jest.spyOn(SfCliTelemetryEmitter.prototype, 'emitTelemetry').mockImplementation((source, eventName, data) => {
33+
receivedTelemetryEmissions.push({source, eventName, data});
34+
return Promise.resolve();
35+
});
2236
const originalCreateAction = RunAction.createAction;
2337
createActionSpy = jest.spyOn(RunAction, 'createAction').mockImplementation((dependencies) => {
2438
receivedActionDependencies = dependencies;
@@ -325,6 +339,14 @@ describe('`code-analyzer run` tests', () => {
325339
});
326340
});
327341

342+
describe('Telemetry emission', () => {
343+
it('Passes telemetry emitter through into Action layer', async () => {
344+
await RunCommand.run([]);
345+
expect(createActionSpy).toHaveBeenCalled();
346+
expect(receivedActionDependencies.telemetryEmitter!.constructor.name).toEqual('SfCliTelemetryEmitter');
347+
});
348+
});
349+
328350
describe('Flag interactions', () => {
329351
describe('--output-file and --view', () => {
330352
it('When --output-file and --view are both present, both are used', async () => {

test/lib/actions/RunAction.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
StubEnginePluginsFactory_withPreconfiguredStubEngines,
1515
StubEnginePluginsFactory_withThrowingStubPlugin
1616
} from '../../stubs/StubEnginePluginsFactories';
17+
import {SpyTelemetryEmitter} from "../../stubs/SpyTelemetryEmitter";
1718

1819
const PATH_TO_FILE_A = path.resolve('test', 'sample-code', 'fileA.cls');
1920
const PATH_TO_GOLDFILES = path.join(__dirname, '..', '..', 'fixtures', 'comparison-files', 'lib', 'actions', 'RunAction.test.ts');
@@ -49,6 +50,7 @@ describe('RunAction tests', () => {
4950
pluginsFactory: pluginsFactory,
5051
logEventListeners: [],
5152
progressListeners: [],
53+
telemetryEmitter: new SpyTelemetryEmitter(),
5254
writer,
5355
resultsViewer,
5456
actionSummaryViewer
@@ -230,6 +232,7 @@ describe('RunAction tests', () => {
230232
pluginsFactory: new StubEnginePluginsFactory_withThrowingStubPlugin(),
231233
logEventListeners: [],
232234
progressListeners: [],
235+
telemetryEmitter: new SpyTelemetryEmitter(),
233236
writer,
234237
resultsViewer,
235238
actionSummaryViewer
@@ -363,6 +366,47 @@ describe('RunAction tests', () => {
363366
expect(displayedLogEvents).toContain(goldfileContents);
364367
});
365368
});
369+
370+
describe('Telemetry Emission', () => {
371+
it('When a telemetry emitter is provided, it is used', async () => {
372+
// ==== SETUP ====
373+
// Create a telemetry emitter and set it to be used.
374+
const spyTelemetryEmitter: SpyTelemetryEmitter = new SpyTelemetryEmitter();
375+
dependencies.telemetryEmitter = spyTelemetryEmitter;
376+
// Create the input.
377+
const input: RunInput = {
378+
// Select all rules.
379+
'rule-selector': ['all'],
380+
// Use the current directory, for convenience.
381+
'workspace': ['.'],
382+
// Outfiles can just be an empty list.
383+
'output-file': []
384+
};
385+
// ==== TESTED BEHAVIOR ====
386+
await action.execute(input);
387+
388+
// ==== ASSERTIONS ====
389+
expect(spyTelemetryEmitter.getCapturedTelemetry()).toHaveLength(4);
390+
391+
expect(spyTelemetryEmitter.getCapturedTelemetry()[0].eventName).toEqual('plugin-code-analyzer');
392+
expect(spyTelemetryEmitter.getCapturedTelemetry()[0].source).toEqual('stubEngine1');
393+
expect(spyTelemetryEmitter.getCapturedTelemetry()[0].data.sfcaEvent).toEqual('engine1DescribeTelemetry');
394+
395+
expect(spyTelemetryEmitter.getCapturedTelemetry()[1].eventName).toEqual('plugin-code-analyzer');
396+
expect(spyTelemetryEmitter.getCapturedTelemetry()[1].source).toEqual('stubEngine1');
397+
expect(spyTelemetryEmitter.getCapturedTelemetry()[1].data.sfcaEvent).toEqual('engine1ExecuteTelemetry');
398+
399+
expect(spyTelemetryEmitter.getCapturedTelemetry()[2].eventName).toEqual('plugin-code-analyzer');
400+
expect(spyTelemetryEmitter.getCapturedTelemetry()[2].source).toEqual('RunAction');
401+
expect(spyTelemetryEmitter.getCapturedTelemetry()[2].data.sfcaEvent).toEqual('engine_selection');
402+
expect(spyTelemetryEmitter.getCapturedTelemetry()[2].data.ruleCount).toEqual(5);
403+
404+
expect(spyTelemetryEmitter.getCapturedTelemetry()[3].eventName).toEqual('plugin-code-analyzer');
405+
expect(spyTelemetryEmitter.getCapturedTelemetry()[3].source).toEqual('RunAction');
406+
expect(spyTelemetryEmitter.getCapturedTelemetry()[3].data.sfcaEvent).toEqual('engine_execution');
407+
expect(spyTelemetryEmitter.getCapturedTelemetry()[3].data.violationCount).toEqual(0);
408+
});
409+
})
366410
});
367411

368412
// TODO: Whenever we decide to document the custom_engine_plugin_modules flag in our configuration file, then we'll want

test/stubs/SpyTelemetryEmitter.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {TelemetryData} from '@salesforce/code-analyzer-core';
2+
import {TelemetryEmitter} from '../../src/lib/Telemetry';
3+
4+
export type CapturedTelemetryEmission = {
5+
source: string,
6+
eventName: string,
7+
data: TelemetryData
8+
};
9+
10+
export class SpyTelemetryEmitter implements TelemetryEmitter {
11+
private capturedTelemetry: CapturedTelemetryEmission[] = [];
12+
13+
public emitTelemetry(source: string, eventName: string, data: TelemetryData): void {
14+
this.capturedTelemetry.push({source, eventName, data});
15+
}
16+
17+
public getCapturedTelemetry(): CapturedTelemetryEmission[] {
18+
return this.capturedTelemetry;
19+
}
20+
}

0 commit comments

Comments
 (0)