Skip to content

Commit 269986e

Browse files
Implement telemetry reporting in extension (microsoft#2613)
1 parent 1ebc5a7 commit 269986e

16 files changed

Lines changed: 1155 additions & 18 deletions

File tree

_extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"bundle:release": "npm run bundle -- --minify"
161161
},
162162
"dependencies": {
163+
"@vscode/extension-telemetry": "^1.2.0",
163164
"vscode-languageclient": "^10.0.0-next.19"
164165
},
165166
"devDependencies": {

_extension/src/client.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import * as vscode from "vscode";
2+
23
import {
4+
CloseAction,
5+
CloseHandlerResult,
6+
ErrorAction,
7+
ErrorHandler,
8+
ErrorHandlerResult,
39
LanguageClient,
410
LanguageClientOptions,
11+
Message,
512
NotebookDocumentFilter,
613
ServerOptions,
714
TextDocumentFilter,
815
TransportKind,
916
} from "vscode-languageclient/node";
17+
1018
import { codeLensShowLocationsCommandName } from "./commands";
1119
import { registerTagClosingFeature } from "./languageFeatures/tagClosing";
20+
import * as tr from "./telemetryReporting";
1221
import {
1322
ExeInfo,
1423
getExe,
@@ -19,6 +28,8 @@ import { getLanguageForUri } from "./util";
1928
export class Client {
2029
private outputChannel: vscode.LogOutputChannel;
2130
private traceOutputChannel: vscode.LogOutputChannel;
31+
private telemetryReporter: tr.TelemetryReporter;
32+
2233
private documentSelector: Array<{ scheme: string; language: string; }>;
2334
private clientOptions: LanguageClientOptions;
2435
private client?: LanguageClient;
@@ -29,9 +40,10 @@ export class Client {
2940
private exe: ExeInfo | undefined;
3041
private onStartedCallbacks: Set<() => void> = new Set();
3142

32-
constructor(outputChannel: vscode.LogOutputChannel, traceOutputChannel: vscode.LogOutputChannel) {
43+
constructor(outputChannel: vscode.LogOutputChannel, traceOutputChannel: vscode.LogOutputChannel, telemetryReporter: tr.TelemetryReporter) {
3344
this.outputChannel = outputChannel;
3445
this.traceOutputChannel = traceOutputChannel;
46+
this.telemetryReporter = telemetryReporter;
3547
this.documentSelector = [
3648
...jsTsLanguageModes.map(language => ({ scheme: "file", language })),
3749
...jsTsLanguageModes.map(language => ({ scheme: "untitled", language })),
@@ -43,6 +55,7 @@ export class Client {
4355
initializationOptions: {
4456
codeLensShowLocationsCommandName,
4557
},
58+
errorHandler: new ReportingErrorHandler(this.telemetryReporter, 5),
4659
diagnosticPullOptions: {
4760
onChange: true,
4861
onSave: true,
@@ -100,6 +113,9 @@ export class Client {
100113
async start(context: vscode.ExtensionContext, exe: { path: string; version: string; }): Promise<vscode.Disposable> {
101114
this.exe = exe;
102115
this.outputChannel.appendLine(`Resolved to ${this.exe.path}`);
116+
this.telemetryReporter.sendTelemetryEvent("languageServer.start", {
117+
version: this.exe.version,
118+
});
103119

104120
// Get pprofDir
105121
const config = vscode.workspace.getConfiguration("typescript.native-preview");
@@ -150,7 +166,32 @@ export class Client {
150166
this.traceOutputChannel.appendLine(`To see LSP trace output, set this output's log level to "Trace" (gear icon next to the dropdown).`);
151167
}
152168

169+
type TelemetryData = {
170+
eventName: string;
171+
telemetryPurpose: "usage" | "error";
172+
properties?: Record<string, string>;
173+
measurements?: Record<string, number>;
174+
};
175+
176+
const serverTelemetryListener = this.client.onTelemetry((d: TelemetryData) => {
177+
switch (d.telemetryPurpose) {
178+
case "usage":
179+
this.telemetryReporter.sendTelemetryEventUntyped(d.eventName, d.properties, d.measurements);
180+
break;
181+
case "error":
182+
this.telemetryReporter.sendTelemetryErrorEventUntyped(d.eventName, d.properties, d.measurements);
183+
break;
184+
default:
185+
const _: never = d.telemetryPurpose;
186+
this.telemetryReporter.sendTelemetryErrorEvent("languageServer.unexpectedTelemetryPurpose", {
187+
telemetryPurpose: String(d.telemetryPurpose),
188+
});
189+
break;
190+
}
191+
});
192+
153193
this.disposables.push(
194+
serverTelemetryListener,
154195
registerTagClosingFeature("typescript", this.documentSelector, this.client),
155196
registerTagClosingFeature("javascript", this.documentSelector, this.client),
156197
);
@@ -186,6 +227,7 @@ export class Client {
186227
}
187228

188229
this.onStartedCallbacks.add(callback);
230+
189231
return new vscode.Disposable(() => {
190232
this.onStartedCallbacks.delete(callback);
191233
});
@@ -247,3 +289,83 @@ export class Client {
247289
return result.file;
248290
}
249291
}
292+
293+
// Adapted from the default error handler in vscode-languageclient.
294+
class ReportingErrorHandler implements ErrorHandler {
295+
telemetryReporter: tr.TelemetryReporter;
296+
maxRestartCount: number;
297+
restarts: number[];
298+
299+
constructor(telemetryReporter: tr.TelemetryReporter, maxRestartCount: number) {
300+
this.telemetryReporter = telemetryReporter;
301+
this.maxRestartCount = maxRestartCount;
302+
this.restarts = [];
303+
}
304+
305+
error(_error: Error, _message: Message | undefined, count: number | undefined): ErrorHandlerResult | Promise<ErrorHandlerResult> {
306+
let errorAction = ErrorAction.Shutdown;
307+
if (count && count <= 3) {
308+
errorAction = ErrorAction.Continue;
309+
}
310+
311+
let actionString = "";
312+
switch (errorAction) {
313+
case ErrorAction.Continue:
314+
actionString = "continue";
315+
break;
316+
case ErrorAction.Shutdown:
317+
actionString = "shutdown";
318+
break;
319+
default:
320+
const _: never = errorAction;
321+
}
322+
this.telemetryReporter.sendTelemetryErrorEvent("languageServer.connectionError", {
323+
resultingAction: actionString,
324+
});
325+
326+
return { action: errorAction };
327+
}
328+
329+
closed(): CloseHandlerResult | Promise<CloseHandlerResult> {
330+
let resultingAction: CloseAction;
331+
332+
this.restarts.push(Date.now());
333+
if (this.restarts.length <= this.maxRestartCount) {
334+
resultingAction = CloseAction.Restart;
335+
}
336+
else {
337+
const diff = this.restarts[this.restarts.length - 1] - this.restarts[0];
338+
if (diff <= 3 * 60 * 1000) {
339+
resultingAction = CloseAction.DoNotRestart;
340+
}
341+
else {
342+
this.restarts.shift();
343+
resultingAction = CloseAction.Restart;
344+
}
345+
}
346+
347+
let actionString = "";
348+
switch (resultingAction) {
349+
case CloseAction.DoNotRestart:
350+
actionString = "doNotRestart";
351+
break;
352+
case CloseAction.Restart:
353+
actionString = "restart";
354+
break;
355+
default:
356+
const _: never = resultingAction;
357+
}
358+
this.telemetryReporter.sendTelemetryErrorEvent("languageServer.connectionClosed", {
359+
resultingAction: actionString,
360+
});
361+
362+
if (resultingAction === CloseAction.DoNotRestart) {
363+
return {
364+
action: resultingAction,
365+
message: `The typescript.native-preview-lsp server crashed ${this.maxRestartCount + 1} times in the last 3 minutes. The server will not be restarted. See the output for more information.`,
366+
};
367+
}
368+
369+
return { action: resultingAction };
370+
}
371+
}

_extension/src/commands.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
11
import * as vscode from "vscode";
2+
23
import type {
34
DocumentUri,
45
Location,
56
Position,
67
} from "vscode-languageclient";
78

89
import { Client } from "./client";
10+
import type * as tr from "./telemetryReporting";
911
import { restartExtHostOnChangeIfNeeded } from "./util";
1012

11-
export function registerEnablementCommands(context: vscode.ExtensionContext): void {
13+
export function registerEnablementCommands(context: vscode.ExtensionContext, telemetryReporter: tr.TelemetryReporter): void {
1214
context.subscriptions.push(vscode.commands.registerCommand("typescript.native-preview.enable", () => {
1315
// Fire and forget, because this will restart the extension host and cause an error if we await
16+
telemetryReporter.sendTelemetryEvent("command.enableNativePreview");
1417
updateUseTsgoSetting(true);
1518
}));
1619

1720
context.subscriptions.push(vscode.commands.registerCommand("typescript.native-preview.disable", () => {
1821
// Fire and forget, because this will restart the extension host and cause an error if we await
22+
telemetryReporter.sendTelemetryEvent("command.disableNativePreview");
1923
updateUseTsgoSetting(false);
2024
}));
2125
}
2226

23-
export function registerLanguageCommands(context: vscode.ExtensionContext, client: Client, outputChannel: vscode.OutputChannel, traceOutputChannel: vscode.OutputChannel): vscode.Disposable[] {
27+
export function registerLanguageCommands(
28+
context: vscode.ExtensionContext,
29+
client: Client,
30+
outputChannel: vscode.OutputChannel,
31+
traceOutputChannel: vscode.OutputChannel,
32+
telemetryReporter: tr.TelemetryReporter,
33+
): vscode.Disposable[] {
2434
const disposables: vscode.Disposable[] = [];
2535

2636
disposables.push(vscode.commands.registerCommand("typescript.native-preview.restart", () => {
37+
telemetryReporter.sendTelemetryEvent("command.restartLanguageServer");
2738
return client.restart(context);
2839
}));
2940

@@ -41,6 +52,7 @@ export function registerLanguageCommands(context: vscode.ExtensionContext, clien
4152
disposables.push(vscode.commands.registerCommand("typescript.native-preview.showMenu", showCommands));
4253

4354
disposables.push(vscode.commands.registerCommand("typescript.native-preview.reportIssue", () => {
55+
telemetryReporter.sendTelemetryEvent("command.reportIssue");
4456
vscode.commands.executeCommand("workbench.action.openIssueReporter", {
4557
extensionId: "TypeScriptTeam.native-preview",
4658
});

_extension/src/extension.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,26 @@ import {
77
registerLanguageCommands,
88
} from "./commands";
99
import { setupStatusBar } from "./statusBar";
10-
import { needsExtHostRestartOnChange } from "./util";
10+
import {
11+
aiConnectionString,
12+
needsExtHostRestartOnChange,
13+
} from "./util";
1114
import { setupVersionStatusItem } from "./versionStatusItem";
1215

16+
import { TelemetryReporter as VSCodeTelemetryReporter } from "@vscode/extension-telemetry";
17+
import {
18+
createTelemetryReporter,
19+
TelemetryReporter,
20+
} from "./telemetryReporting";
21+
1322
export async function activate(context: vscode.ExtensionContext) {
1423
await vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", false);
15-
registerEnablementCommands(context);
24+
25+
const telemetryReporter = createTelemetryReporter(new VSCodeTelemetryReporter(aiConnectionString));
26+
context.subscriptions.push(telemetryReporter);
27+
28+
registerEnablementCommands(context, telemetryReporter);
29+
1630
const output = vscode.window.createOutputChannel("typescript-native-preview", { log: true });
1731
const traceOutput = vscode.window.createOutputChannel("typescript-native-preview (LSP)", { log: true });
1832
context.subscriptions.push(output, traceOutput);
@@ -34,7 +48,7 @@ export async function activate(context: vscode.ExtensionContext) {
3448
else {
3549
const useTsgo = vscode.workspace.getConfiguration("typescript").get<boolean>("experimental.useTsgo");
3650
if (useTsgo) {
37-
disposeLanguageFeatures = await activateLanguageFeatures(context, output, traceOutput);
51+
disposeLanguageFeatures = await activateLanguageFeatures(context, output, traceOutput, telemetryReporter);
3852
context.subscriptions.push(disposeLanguageFeatures);
3953
}
4054
else {
@@ -74,15 +88,20 @@ export async function activate(context: vscode.ExtensionContext) {
7488
return;
7589
}
7690

77-
disposeLanguageFeatures = await activateLanguageFeatures(context, output, traceOutput);
91+
disposeLanguageFeatures = await activateLanguageFeatures(context, output, traceOutput, telemetryReporter);
7892
context.subscriptions.push(disposeLanguageFeatures);
7993
}
8094

81-
async function activateLanguageFeatures(context: vscode.ExtensionContext, output: vscode.LogOutputChannel, traceOutput: vscode.LogOutputChannel): Promise<vscode.Disposable> {
95+
async function activateLanguageFeatures(
96+
context: vscode.ExtensionContext,
97+
output: vscode.LogOutputChannel,
98+
traceOutput: vscode.LogOutputChannel,
99+
telemetryReporter: TelemetryReporter,
100+
): Promise<vscode.Disposable> {
82101
const disposables: vscode.Disposable[] = [];
83102

84-
const client = new Client(output, traceOutput);
85-
disposables.push(...registerLanguageCommands(context, client, output, traceOutput));
103+
const client = new Client(output, traceOutput, telemetryReporter);
104+
disposables.push(...registerLanguageCommands(context, client, output, traceOutput, telemetryReporter));
86105
disposables.push(await client.initialize(context));
87106
disposables.push(setupStatusBar());
88107
disposables.push(...setupVersionStatusItem(client));
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { TelemetryReporter as VSCodeTelemetryReporter } from "@vscode/extension-telemetry";
2+
3+
// As new events are added, update the TelemetryReporter interface below.
4+
// This helps ensure that the telemetry events used in the codebase are
5+
// properly typed, and that properties/measurements are never forgotten.
6+
//
7+
// The difference between `sendTelemetryEvent` and `sendTelemetryErrorEvent` is that
8+
// these methods respect user preferences around reporting (i.e. `telemetry.telemetryLevel`).
9+
//
10+
// The "untyped" variants are provided for when properties/measurements are not known
11+
// from the editor client - for example, when forwarding telemetry events from the language server.
12+
export interface TelemetryReporter {
13+
sendTelemetryEvent(eventName: "command.enableNativePreview"): void;
14+
sendTelemetryEvent(eventName: "command.disableNativePreview"): void;
15+
sendTelemetryEvent(eventName: "command.restartLanguageServer"): void;
16+
sendTelemetryEvent(eventName: "command.reportIssue"): void;
17+
sendTelemetryEvent(eventName: "languageServer.start", data: LSServerStart): void;
18+
19+
sendTelemetryErrorEvent(eventName: "languageServer.connectionError", data: LSConnectionError): void;
20+
sendTelemetryErrorEvent(eventName: "languageServer.connectionClosed", data: LSServerConnectionClosed): void;
21+
sendTelemetryErrorEvent(eventName: "languageServer.errorResponse", data: LSErrorResponse): void;
22+
sendTelemetryErrorEvent(eventName: "languageServer.unexpectedTelemetryPurpose", data: UnexpectedTelemetryPurpose): void;
23+
24+
sendTelemetryEventUntyped(eventName: string, data?: Record<string, string>, measurements?: Record<string, number>): void;
25+
sendTelemetryErrorEventUntyped(eventName: string, data?: Record<string, string>, measurements?: Record<string, number>): void;
26+
27+
dispose(): void;
28+
}
29+
30+
export function createTelemetryReporter(vscReporter: VSCodeTelemetryReporter): TelemetryReporter {
31+
return {
32+
sendTelemetryEvent,
33+
sendTelemetryErrorEvent,
34+
sendTelemetryEventUntyped: sendTelemetryEvent,
35+
sendTelemetryErrorEventUntyped: sendTelemetryErrorEvent,
36+
37+
dispose: () => vscReporter.dispose(),
38+
};
39+
40+
function sendTelemetryEvent(eventName: string, data?: Record<string, string>, measurements?: Record<string, number>): void {
41+
vscReporter.sendTelemetryEvent(eventName, data, measurements);
42+
}
43+
44+
function sendTelemetryErrorEvent(eventName: string, data?: Record<string, string>, measurements?: Record<string, number>): void {
45+
vscReporter.sendTelemetryErrorEvent(eventName, data, measurements);
46+
}
47+
}
48+
49+
export type LSServerStart = {
50+
version: string;
51+
};
52+
53+
export type LSConnectionError = {
54+
resultingAction: string;
55+
};
56+
57+
export type LSServerConnectionClosed = {
58+
resultingAction: string;
59+
};
60+
61+
export type LSErrorResponse = {
62+
errorCode: string;
63+
requestMethod: string;
64+
stack: string;
65+
};
66+
67+
export type EnableNativePreview = {};
68+
69+
export type DisableNativePreview = {};
70+
71+
export type RestartLanguageServer = {};
72+
73+
export type ReportIssue = {};
74+
75+
export type UnexpectedTelemetryPurpose = {
76+
telemetryPurpose: string;
77+
};

_extension/src/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import packageJson from "../package.json";
44

55
const version = packageJson.version;
66

7+
export const aiConnectionString = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255";
8+
79
export const jsTsLanguageModes = [
810
"typescript",
911
"typescriptreact",

0 commit comments

Comments
 (0)