Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/out/src/**/*.js"],
"preLaunchTask": "compile typescript"
"preLaunchTask": "compile typescript",
"env": {
"VSCODE_REDHAT_TELEMETRY_DEBUG":"true"
}
},
{
"name": "Extension Tests",
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ e.g.
}
```

## Data and Telemetry

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-redhat-telemetry#how-to-disable-telemetry-reporting

## How to contribute

The instructions are available in the [contribution guide](CONTRIBUTING.md).
4 changes: 2 additions & 2 deletions USAGE_DATA.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Data collection

vscode-yaml has opt-in telemetry collection, provided by [vscode-commons](https://github.com/redhat-developer/vscode-commons).
vscode-yaml has opt-in telemetry collection, provided by [vscode-redhat-telemetry](https://github.com/redhat-developer/vscode-redhat-telemetry).

## What's included in the vscode-yaml telemetry data

Expand All @@ -12,7 +12,7 @@ vscode-yaml has opt-in telemetry collection, provided by [vscode-commons](https:
## What's included in the general telemetry data

Please see the
[vscode-commons data collection information](https://github.com/redhat-developer/vscode-commons/blob/master/USAGE_DATA.md#other-extensions)
[vscode-redhat-telemetry data collection information](https://github.com/redhat-developer/vscode-redhat-telemetry/blob/HEAD/USAGE_DATA.md)
for information on what data it collects.

## How to opt in or out
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@
],
"configuration": {
"properties": {
"redhat.telemetry.enabled": {
"type": "boolean",
"default": null,
"markdownDescription": "Enable usage data and errors to be sent to Red Hat servers. Read our [privacy statement](https://developers.redhat.com/article/tool-data-collection).",
"scope": "window"
},
"yaml.trace.server": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -210,6 +216,7 @@
"vscode-test": "^1.4.0"
},
"dependencies": {
"@redhat-developer/vscode-redhat-telemetry": "0.1.1",
"fs-extra": "^9.1.0",
"request-light": "^0.4.0",
"vscode-languageclient": "7.0.0",
Expand Down
18 changes: 17 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { joinPath } from './paths';
import { getJsonSchemaContent, JSONSchemaDocumentContentProvider } from './json-schema-content-provider';
import { JSONSchemaCache } from './json-schema-cache';
import { getConflictingExtensions, showUninstallConflictsNotification } from './extensionConflicts';
import { getRedHatService } from '@redhat-developer/vscode-redhat-telemetry';
import { TelemetryErrorHandler, TelemetryOutputChannel } from './telemetry';

export interface ISchemaAssociations {
[pattern: string]: string[];
Expand Down Expand Up @@ -80,6 +82,10 @@ let client: LanguageClient;
const lsName = 'YAML Support';

export async function activate(context: ExtensionContext): Promise<SchemaExtensionAPI> {
// Create Telemetry Service
const telemetry = await (await getRedHatService(context)).getTelemetryService();
telemetry.sendStartupEvent();

// The YAML language server is implemented in node
const serverModule = context.asAbsolutePath(
path.join('node_modules', 'yaml-language-server', 'out', 'server', 'src', 'server.js')
Expand All @@ -95,6 +101,8 @@ export async function activate(context: ExtensionContext): Promise<SchemaExtensi
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions },
};

const telemetryErrorHandler = new TelemetryErrorHandler(telemetry, lsName, 4);
const outputChannel = window.createOutputChannel(lsName);
// Options to control the language client
const clientOptions: LanguageClientOptions = {
// Register the server for on disk and newly created YAML documents
Expand All @@ -104,6 +112,8 @@ export async function activate(context: ExtensionContext): Promise<SchemaExtensi
fileEvents: [workspace.createFileSystemWatcher('**/*.?(e)y?(a)ml'), workspace.createFileSystemWatcher('**/*.json')],
},
revealOutputChannelOn: RevealOutputChannelOn.Never,
errorHandler: telemetryErrorHandler,
outputChannel: new TelemetryOutputChannel(outputChannel, telemetry),
};

// Create the language client and start it
Expand All @@ -124,6 +134,12 @@ export async function activate(context: ExtensionContext): Promise<SchemaExtensi
)
);

context.subscriptions.push(
client.onTelemetry((e) => {
telemetry.send(e);
})
);

findConflicts();
client.onReady().then(() => {
// Send a notification to the server with any YAML schema associations in all extensions
Expand All @@ -149,7 +165,7 @@ export async function activate(context: ExtensionContext): Promise<SchemaExtensi
return getJsonSchemaContent(uri, schemaCache);
});

// telemetry.send({ name: 'yaml.server.initialized' });
telemetry.send({ name: 'yaml.server.initialized' });
// Adapted from:
// https://github.com/microsoft/vscode/blob/94c9ea46838a9a619aeafb7e8afd1170c967bb55/extensions/json-language-features/client/src/jsonClient.ts#L305-L318
client.onNotification(ResultLimitReachedNotification.type, async (message) => {
Expand Down
80 changes: 80 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TelemetryService } from '@redhat-developer/vscode-redhat-telemetry';
import { CloseAction, ErrorAction, ErrorHandler, Message } from 'vscode-languageclient/node';
import * as vscode from 'vscode';

export class TelemetryErrorHandler implements ErrorHandler {
private restarts: number[] = [];
constructor(
private readonly telemetry: TelemetryService,
private readonly name: string,
private readonly maxRestartCount: number
) {}

error(error: Error, message: Message, count: number): ErrorAction {
this.telemetry.send({ name: 'yaml.lsp.error', properties: { jsonrpc: message.jsonrpc, error: error.message } });
if (count && count <= 3) {
return ErrorAction.Continue;
}
return ErrorAction.Shutdown;
}
closed(): CloseAction {
this.restarts.push(Date.now());
if (this.restarts.length <= this.maxRestartCount) {
return CloseAction.Restart;
} else {
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
if (diff <= 3 * 60 * 1000) {
vscode.window.showErrorMessage(
`The ${this.name} server crashed ${
this.maxRestartCount + 1
} times in the last 3 minutes. The server will not be restarted.`
);
return CloseAction.DoNotRestart;
} else {
this.restarts.shift();
return CloseAction.Restart;
}
}
}
}

export class TelemetryOutputChannel implements vscode.OutputChannel {
constructor(private readonly delegate: vscode.OutputChannel, private readonly telemetry: TelemetryService) {}

get name(): string {
return this.delegate.name;
}
append(value: string): void {
this.checkError(value);
this.delegate.append(value);
}
appendLine(value: string): void {
this.checkError(value);
this.delegate.appendLine(value);
}

private checkError(value: string): void {
if (value.startsWith('[Error') || value.startsWith(' Message: Request')) {
this.telemetry.send({ name: 'yaml.server.error', properties: { error: value } });
}
}
clear(): void {
this.delegate.clear();
}
show(preserveFocus?: boolean): void;
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
show(column?: never, preserveFocus?: boolean): void {
this.delegate.show(column, preserveFocus);
}
hide(): void {
this.delegate.hide();
}
dispose(): void {
this.delegate.dispose();
}
}
109 changes: 109 additions & 0 deletions test/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';
import * as chai from 'chai';
import * as vscode from 'vscode';
import { TelemetryErrorHandler, TelemetryOutputChannel } from '../src/telemetry';
import { TelemetryEvent, TelemetryService } from '@redhat-developer/vscode-redhat-telemetry/lib/interfaces/telemetry';

const expect = chai.expect;
chai.use(sinonChai);
class TelemetryStub implements TelemetryService {
sendStartupEvent(): Promise<void> {
throw new Error('Method not implemented.');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
send(event: TelemetryEvent): Promise<void> {
throw new Error('Method not implemented.');
}
sendShutdownEvent(): Promise<void> {
throw new Error('Method not implemented.');
}
flushQueue(): Promise<void> {
throw new Error('Method not implemented.');
}
dispose(): Promise<void> {
throw new Error('Method not implemented.');
}
}
describe('Telemetry Test', () => {
const sandbox = sinon.createSandbox();
const testOutputChannel = vscode.window.createOutputChannel('YAML_TEST');
afterEach(() => {
sandbox.restore();
});
describe('TelemetryOutputChannel', () => {
let telemetryChannel: TelemetryOutputChannel;
let outputChannel: sinon.SinonStubbedInstance<vscode.OutputChannel>;
let telemetry: sinon.SinonStubbedInstance<TelemetryService>;
beforeEach(() => {
outputChannel = sandbox.stub(testOutputChannel);
telemetry = sandbox.stub(new TelemetryStub());
telemetryChannel = new TelemetryOutputChannel(
(outputChannel as unknown) as vscode.OutputChannel,
(telemetry as unknown) as TelemetryService
);
});

it('should delegate "append" method', () => {
telemetryChannel.append('Some');
expect(outputChannel.append).calledOnceWith('Some');
});

it('should delegate "appendLine" method', () => {
telemetryChannel.appendLine('Some');
expect(outputChannel.appendLine).calledOnceWith('Some');
});

it('should delegate "clear" method', () => {
telemetryChannel.clear();
expect(outputChannel.clear).calledOnce;
});

it('should delegate "dispose" method', () => {
telemetryChannel.dispose();
expect(outputChannel.dispose).calledOnce;
});

it('should delegate "hide" method', () => {
telemetryChannel.hide();
expect(outputChannel.hide).calledOnce;
});

it('should delegate "show" method', () => {
telemetryChannel.show();
expect(outputChannel.show).calledOnce;
});

it('should send telemetry if log error in "append"', () => {
telemetryChannel.append('[Error Some');
expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: '[Error Some' } });
});

it('should send telemetry if log error on "appendLine"', () => {
telemetryChannel.appendLine('[Error Some');
expect(telemetry.send).calledOnceWith({ name: 'yaml.server.error', properties: { error: '[Error Some' } });
});
});

describe('TelemetryErrorHandler', () => {
let telemetry: sinon.SinonStubbedInstance<TelemetryService>;
let errorHandler: TelemetryErrorHandler;

beforeEach(() => {
telemetry = sandbox.stub(new TelemetryStub());
errorHandler = new TelemetryErrorHandler(telemetry, 'YAML LS', 3);
});

it('should log telemetry on error', () => {
errorHandler.error(new Error('Some'), { jsonrpc: 'Error message' }, 3);
expect(telemetry.send).calledOnceWith({
name: 'yaml.lsp.error',
properties: { jsonrpc: 'Error message', error: 'Some' },
});
});
});
});
Loading