Skip to content

Commit fec9024

Browse files
authored
Add telemetry (#490)
* #458 add telemetry Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix test Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Fix review comments Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix build Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * Rename test and constant Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com> * fix typo Signed-off-by: Yevhen Vydolob <yvydolob@redhat.com>
1 parent 9603a1b commit fec9024

File tree

9 files changed

+688
-10
lines changed

9 files changed

+688
-10
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"request": "launch",
2929
"runtimeExecutable": "${execPath}",
3030
"args": [
31-
"--disable-extensions",
31+
"--disable-extension=ms-kubernetes-tools.vscode-kubernetes-tools",
3232
"--extensionDevelopmentPath=${workspaceFolder}",
3333
"--extensionTestsPath=${workspaceFolder}/out/test",
3434
"${workspaceRoot}/test/testFixture"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ e.g.
233233
}
234234
```
235235

236+
## Data and Telemetry
237+
238+
The `vscode-yaml` extension collects anonymous [usage data](USAGE_DATA.md) and sends it to Red Hat servers to help improve our products and services. Read our [privacy statement](https://developers.redhat.com/article/tool-data-collection) to learn more. This extension respects the `redhat.telemetry.enabled` setting which you can learn more about at https://github.com/redhat-developer/vscode-commons#how-to-disable-telemetry-reporting
239+
236240
## How to contribute
237241

238242
The instructions are available in the [contribution guide](CONTRIBUTING.md).

USAGE_DATA.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Data collection
2+
3+
vscode-yaml has opt-in telemetry collection, provided by [vscode-commons](https://github.com/redhat-developer/vscode-commons).
4+
5+
## What's included in the vscode-yaml telemetry data
6+
7+
- yaml-language-server start
8+
- errors during yaml language server start
9+
- any errors from LSP requests
10+
- `kubernetes` schema usage
11+
12+
## What's included in the general telemetry data
13+
14+
Please see the
15+
[vscode-commons data collection information](https://github.com/redhat-developer/vscode-commons/blob/master/USAGE_DATA.md#other-extensions)
16+
for information on what data it collects.
17+
18+
## How to opt in or out
19+
20+
Use the `redhat.telemetry.enabled` setting in order to enable or disable telemetry collection.

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@
174174
}
175175
}
176176
},
177+
"extensionDependencies": [
178+
"redhat.vscode-commons"
179+
],
177180
"scripts": {
178181
"build": "yarn run clean && yarn run lint && yarn run vscode:prepublish",
179182
"check-dependencies": "node ./scripts/check-dependencies.js",
@@ -185,24 +188,31 @@
185188
"vscode:prepublish": "tsc -p ./"
186189
},
187190
"devDependencies": {
191+
"@types/chai": "^4.2.12",
188192
"@types/fs-extra": "^9.0.6",
189193
"@types/mocha": "^2.2.48",
190194
"@types/node": "^12.12.6",
195+
"@types/sinon": "^9.0.5",
196+
"@types/sinon-chai": "^3.2.5",
191197
"@types/vscode": "^1.52.0",
192198
"@typescript-eslint/eslint-plugin": "^4.16.1",
193199
"@typescript-eslint/parser": "^4.16.1",
200+
"chai": "^4.2.0",
194201
"eslint": "^7.6.0",
195202
"eslint-config-prettier": "^6.11.0",
196203
"eslint-plugin-prettier": "^3.1.4",
197204
"glob": "^7.1.6",
198205
"mocha": "^8.0.1",
199206
"prettier": "^2.0.5",
200207
"rimraf": "^3.0.2",
208+
"sinon": "^9.0.3",
209+
"sinon-chai": "^3.5.0",
201210
"ts-node": "^3.3.0",
202211
"typescript": "4.1.2",
203212
"vscode-test": "^1.4.0"
204213
},
205214
"dependencies": {
215+
"@redhat-developer/vscode-redhat-telemetry": "0.0.18",
206216
"fs-extra": "^9.1.0",
207217
"request-light": "^0.4.0",
208218
"vscode-languageclient": "7.0.0",

src/extension.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { joinPath } from './paths';
2323
import { getJsonSchemaContent, JSONSchemaDocumentContentProvider } from './json-schema-content-provider';
2424
import { JSONSchemaCache } from './json-schema-cache';
2525
import { getConflictingExtensions, showUninstallConflictsNotification } from './extensionConflicts';
26+
import { getTelemetryService } from '@redhat-developer/vscode-redhat-telemetry';
27+
import { TelemetryErrorHandler, TelemetryOutputChannel } from './telemetry';
2628

2729
export interface ISchemaAssociations {
2830
[pattern: string]: string[];
@@ -77,7 +79,13 @@ namespace ResultLimitReachedNotification {
7779

7880
let client: LanguageClient;
7981

80-
export function activate(context: ExtensionContext): SchemaExtensionAPI {
82+
const lsName = 'YAML Support';
83+
84+
export async function activate(context: ExtensionContext): Promise<SchemaExtensionAPI> {
85+
// Create Telemetry Service
86+
const telemetry = await getTelemetryService('redhat.vscode-yaml');
87+
telemetry.sendStartupEvent();
88+
8189
// The YAML language server is implemented in node
8290
const serverModule = context.asAbsolutePath(
8391
path.join('node_modules', 'yaml-language-server', 'out', 'server', 'src', 'server.js')
@@ -93,6 +101,8 @@ export function activate(context: ExtensionContext): SchemaExtensionAPI {
93101
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions },
94102
};
95103

104+
const telemetryErrorHandler = new TelemetryErrorHandler(telemetry, lsName, 4);
105+
const outputChannel = window.createOutputChannel(lsName);
96106
// Options to control the language client
97107
const clientOptions: LanguageClientOptions = {
98108
// Register the server for on disk and newly created YAML documents
@@ -104,10 +114,12 @@ export function activate(context: ExtensionContext): SchemaExtensionAPI {
104114
fileEvents: [workspace.createFileSystemWatcher('**/*.?(e)y?(a)ml'), workspace.createFileSystemWatcher('**/*.json')],
105115
},
106116
revealOutputChannelOn: RevealOutputChannelOn.Never,
117+
errorHandler: telemetryErrorHandler,
118+
outputChannel: new TelemetryOutputChannel(outputChannel, telemetry),
107119
};
108120

109121
// Create the language client and start it
110-
client = new LanguageClient('yaml', 'YAML Support', serverOptions, clientOptions);
122+
client = new LanguageClient('yaml', lsName, serverOptions, clientOptions);
111123

112124
const schemaCache = new JSONSchemaCache(context.globalStoragePath, context.globalState, client.outputChannel);
113125
const disposable = client.start();
@@ -124,6 +136,12 @@ export function activate(context: ExtensionContext): SchemaExtensionAPI {
124136
)
125137
);
126138

139+
context.subscriptions.push(
140+
client.onTelemetry((e) => {
141+
telemetry.send(e);
142+
})
143+
);
144+
127145
findConflicts();
128146
client.onReady().then(() => {
129147
// Send a notification to the server with any YAML schema associations in all extensions
@@ -149,6 +167,7 @@ export function activate(context: ExtensionContext): SchemaExtensionAPI {
149167
return getJsonSchemaContent(uri, schemaCache);
150168
});
151169

170+
telemetry.send({ name: 'yaml.server.initialized' });
152171
// Adapted from:
153172
// https://github.com/microsoft/vscode/blob/94c9ea46838a9a619aeafb7e8afd1170c967bb55/extensions/json-language-features/client/src/jsonClient.ts#L305-L318
154173
client.onNotification(ResultLimitReachedNotification.type, async (message) => {

src/telemetry.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TelemetryService } from '@redhat-developer/vscode-redhat-telemetry/lib';
7+
import { CloseAction, ErrorAction, ErrorHandler, Message } from 'vscode-languageclient/node';
8+
import * as vscode from 'vscode';
9+
10+
export class TelemetryErrorHandler implements ErrorHandler {
11+
private restarts: number[] = [];
12+
constructor(
13+
private readonly telemetry: TelemetryService,
14+
private readonly name: string,
15+
private readonly maxRestartCount: number
16+
) {}
17+
18+
error(error: Error, message: Message, count: number): ErrorAction {
19+
this.telemetry.send({ name: 'yaml.lsp.error', properties: { jsonrpc: message.jsonrpc, error: error.message } });
20+
if (count && count <= 3) {
21+
return ErrorAction.Continue;
22+
}
23+
return ErrorAction.Shutdown;
24+
}
25+
closed(): CloseAction {
26+
this.restarts.push(Date.now());
27+
if (this.restarts.length <= this.maxRestartCount) {
28+
return CloseAction.Restart;
29+
} else {
30+
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
31+
if (diff <= 3 * 60 * 1000) {
32+
vscode.window.showErrorMessage(
33+
`The ${this.name} server crashed ${
34+
this.maxRestartCount + 1
35+
} times in the last 3 minutes. The server will not be restarted.`
36+
);
37+
return CloseAction.DoNotRestart;
38+
} else {
39+
this.restarts.shift();
40+
return CloseAction.Restart;
41+
}
42+
}
43+
}
44+
}
45+
46+
export class TelemetryOutputChannel implements vscode.OutputChannel {
47+
constructor(private readonly delegate: vscode.OutputChannel, private readonly telemetry: TelemetryService) {}
48+
49+
get name(): string {
50+
return this.delegate.name;
51+
}
52+
append(value: string): void {
53+
this.checkError(value);
54+
this.delegate.append(value);
55+
}
56+
appendLine(value: string): void {
57+
this.checkError(value);
58+
this.delegate.appendLine(value);
59+
}
60+
61+
private checkError(value: string): void {
62+
if (value.startsWith('[Error') || value.startsWith(' Message: Request')) {
63+
this.telemetry.send({ name: 'yaml.server.error', properties: { error: value } });
64+
}
65+
}
66+
clear(): void {
67+
this.delegate.clear();
68+
}
69+
show(preserveFocus?: boolean): void;
70+
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
71+
show(column?: never, preserveFocus?: boolean): void {
72+
this.delegate.show(column, preserveFocus);
73+
}
74+
hide(): void {
75+
this.delegate.hide();
76+
}
77+
dispose(): void {
78+
this.delegate.dispose();
79+
}
80+
}

test/telemetry.test.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat, Inc. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
import * as sinon from 'sinon';
6+
import * as sinonChai from 'sinon-chai';
7+
import * as chai from 'chai';
8+
import * as vscode from 'vscode';
9+
import { TelemetryErrorHandler, TelemetryOutputChannel } from '../src/telemetry';
10+
import { TelemetryEvent, TelemetryService } from '@redhat-developer/vscode-redhat-telemetry/lib/interfaces/telemetry';
11+
12+
const expect = chai.expect;
13+
chai.use(sinonChai);
14+
class TelemetryStub implements TelemetryService {
15+
sendStartupEvent(): Promise<void> {
16+
throw new Error('Method not implemented.');
17+
}
18+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
19+
send(event: TelemetryEvent): Promise<void> {
20+
throw new Error('Method not implemented.');
21+
}
22+
sendShutdownEvent(): Promise<void> {
23+
throw new Error('Method not implemented.');
24+
}
25+
flushQueue(): Promise<void> {
26+
throw new Error('Method not implemented.');
27+
}
28+
dispose(): Promise<void> {
29+
throw new Error('Method not implemented.');
30+
}
31+
}
32+
describe('Telemetry Test', () => {
33+
const sandbox = sinon.createSandbox();
34+
const testOutputChannel = vscode.window.createOutputChannel('YAML_TEST');
35+
afterEach(() => {
36+
sandbox.restore();
37+
});
38+
describe('TelemetryOutputChannel', () => {
39+
let telemetryChannel: TelemetryOutputChannel;
40+
let outputChannel: sinon.SinonStubbedInstance<vscode.OutputChannel>;
41+
let telemetry: sinon.SinonStubbedInstance<TelemetryService>;
42+
beforeEach(() => {
43+
outputChannel = sandbox.stub(testOutputChannel);
44+
telemetry = sandbox.stub(new TelemetryStub());
45+
telemetryChannel = new TelemetryOutputChannel(
46+
(outputChannel as unknown) as vscode.OutputChannel,
47+
(telemetry as unknown) as TelemetryService
48+
);
49+
});
50+
51+
it('should delegate "append" method', () => {
52+
telemetryChannel.append('Some');
53+
expect(outputChannel.append).calledOnceWith('Some');
54+
});
55+
56+
it('should delegate "appendLine" method', () => {
57+
telemetryChannel.appendLine('Some');
58+
expect(outputChannel.appendLine).calledOnceWith('Some');
59+
});
60+
61+
it('should delegate "clear" method', () => {
62+
telemetryChannel.clear();
63+
expect(outputChannel.clear).calledOnce;
64+
});
65+
66+
it('should delegate "dispose" method', () => {
67+
telemetryChannel.dispose();
68+
expect(outputChannel.dispose).calledOnce;
69+
});
70+
71+
it('should delegate "hide" method', () => {
72+
telemetryChannel.hide();
73+
expect(outputChannel.hide).calledOnce;
74+
});
75+
76+
it('should delegate "show" method', () => {
77+
telemetryChannel.show();
78+
expect(outputChannel.show).calledOnce;
79+
});
80+
81+
it('should send telemetry if log error in "append"', () => {
82+
telemetryChannel.append('[Error Some');
83+
expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: '[Error Some' } });
84+
});
85+
86+
it('should send telemetry if log error on "appendLine"', () => {
87+
telemetryChannel.appendLine('[Error Some');
88+
expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: '[Error Some' } });
89+
});
90+
});
91+
92+
describe('TelemetryErrorHandler', () => {
93+
let telemetry: sinon.SinonStubbedInstance<TelemetryService>;
94+
let errorHandler: TelemetryErrorHandler;
95+
96+
beforeEach(() => {
97+
telemetry = sandbox.stub(new TelemetryStub());
98+
errorHandler = new TelemetryErrorHandler(telemetry, 'YAML LS', 3);
99+
});
100+
101+
it('should log telemetry on error', () => {
102+
errorHandler.error(new Error('Some'), { jsonrpc: 'Error message' }, 3);
103+
expect(telemetry.send).calledOnceWith({
104+
name: 'yaml.lsp.error',
105+
properties: { jsonrpc: 'Error message', error: 'Some' },
106+
});
107+
});
108+
});
109+
});

test/testRunner.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
* ------------------------------------------------------------------------------------------ */
55
import * as path from 'path';
6-
7-
import { runTests } from 'vscode-test';
6+
import * as cp from 'child_process';
7+
import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test';
88

99
async function main(): Promise<void> {
1010
try {
11+
const executable = await downloadAndUnzipVSCode();
12+
const cliPath = resolveCliPathFromVSCodeExecutablePath(executable);
13+
const dependencies = ['redhat.vscode-commons'];
14+
for (const dep of dependencies) {
15+
const installLog = cp.execSync(`"${cliPath}" --install-extension ${dep}`);
16+
console.log(installLog.toString());
17+
}
1118
// The folder containing the Extension Manifest package.json
1219
// Passed to `--extensionDevelopmentPath`
1320
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
@@ -18,9 +25,10 @@ async function main(): Promise<void> {
1825

1926
// Download VS Code, unzip it and run the integration test
2027
await runTests({
28+
vscodeExecutablePath: executable,
2129
extensionDevelopmentPath,
2230
extensionTestsPath,
23-
launchArgs: ['--disable-extensions', '.'],
31+
launchArgs: ['--disable-extension=ms-kubernetes-tools.vscode-kubernetes-tools', '.'],
2432
});
2533
} catch (err) {
2634
console.error('Failed to run tests');

0 commit comments

Comments
 (0)