enable sending telemetry events to tsserver client (#12034) (#12051)

enable sending telemetry events
This commit is contained in:
Vladimir Matveev 2016-11-04 11:35:44 -07:00 committed by GitHub
parent afe36be50c
commit f03e04a41e
15 changed files with 219 additions and 59 deletions

View File

@ -171,6 +171,7 @@ var servicesSources = [
var serverCoreSources = [
"types.d.ts",
"shared.ts",
"utilities.ts",
"scriptVersionCache.ts",
"typingsCache.ts",
@ -193,6 +194,7 @@ var cancellationTokenSources = [
var typingsInstallerSources = [
"../types.d.ts",
"../shared.ts",
"typingsInstaller.ts",
"nodeTypingsInstaller.ts"
].map(function (f) {

View File

@ -51,8 +51,9 @@ namespace ts.projectSystem {
throttleLimit: number,
installTypingHost: server.ServerHost,
readonly typesRegistry = createMap<void>(),
telemetryEnabled?: boolean,
log?: TI.Log) {
super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, log);
super(installTypingHost, globalTypingsCacheLocation, safeList.path, throttleLimit, telemetryEnabled, log);
}
safeFileList = safeList.path;

View File

@ -20,12 +20,13 @@ namespace ts.projectSystem {
}
class Installer extends TestTypingsInstaller {
constructor(host: server.ServerHost, p?: InstallerParams, log?: TI.Log) {
constructor(host: server.ServerHost, p?: InstallerParams, telemetryEnabled?: boolean, log?: TI.Log) {
super(
(p && p.globalTypingsCacheLocation) || "/a/data",
(p && p.throttleLimit) || 5,
host,
(p && p.typesRegistry),
telemetryEnabled,
log);
}
@ -35,15 +36,16 @@ namespace ts.projectSystem {
}
}
function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void {
self.addPostExecAction(installedTypings, success => {
for (const file of typingFiles) {
host.createFileOrFolder(file, /*createParentDirectory*/ true);
}
cb(success);
});
}
describe("typingsInstaller", () => {
function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[], typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void {
self.addPostExecAction(installedTypings, success => {
for (const file of typingFiles) {
host.createFileOrFolder(file, /*createParentDirectory*/ true);
}
cb(success);
});
}
it("configured projects (typings installed) 1", () => {
const file1 = {
path: "/a/b/app.js",
@ -905,7 +907,7 @@ namespace ts.projectSystem {
const host = createServerHost([f1, packageJson]);
const installer = new (class extends Installer {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) });
super(host, { globalTypingsCacheLocation: "/tmp" }, /*telemetryEnabled*/ false, { isEnabled: () => true, writeLine: msg => messages.push(msg) });
}
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) {
assert(false, "runCommand should not be invoked");
@ -949,4 +951,50 @@ namespace ts.projectSystem {
assert.deepEqual(result.newTypingNames, ["bar"]);
});
});
describe("telemetry events", () => {
it ("should be received", () => {
const f1 = {
path: "/a/app.js",
content: ""
};
const package = {
path: "/a/package.json",
content: JSON.stringify({ dependencies: { "commander": "1.0.0" } })
};
const cachePath = "/a/cache/";
const commander = {
path: cachePath + "node_modules/@types/commander/index.d.ts",
content: "export let x: number"
};
const host = createServerHost([f1, package]);
let seenTelemetryEvent = false;
const installer = new (class extends Installer {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }, /*telemetryEnabled*/ true);
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
const installedTypings = ["@types/commander"];
const typingFiles = [commander];
executeCommand(this, host, installedTypings, typingFiles, cb);
}
sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.TypingsInstallEvent) {
if (response.kind === server.EventInstall) {
assert.deepEqual(response.packagesToInstall, ["@types/commander"]);
seenTelemetryEvent = true;
return;
}
super.sendResponse(response);
}
})();
const projectService = createProjectService(host, { typingsInstaller: installer });
projectService.openClientFile(f1.path);
installer.installAll(/*expectedCount*/ 1);
assert.isTrue(seenTelemetryEvent);
checkNumberOfProjects(projectService, { inferredProjects: 1 });
checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]);
});
});
}

View File

@ -286,10 +286,10 @@ namespace ts.server {
return;
}
switch (response.kind) {
case "set":
case ActionSet:
this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.unresolvedImports, response.typings);
break;
case "invalidate":
case ActionInvalidate:
this.typingsCache.deleteTypingsForProject(response.projectName);
break;
}

View File

@ -2057,6 +2057,32 @@ namespace ts.server.protocol {
childItems?: NavigationTree[];
}
export type TelemetryEventName = "telemetry";
export interface TelemetryEvent extends Event {
event: TelemetryEventName;
body: TelemetryEventBody;
}
export interface TelemetryEventBody {
telemetryEventName: string;
payload: any;
}
export type TypingsInstalledTelemetryEventName = "typingsInstalled";
export interface TypingsInstalledTelemetryEventBody extends TelemetryEventBody {
telemetryEventName: TypingsInstalledTelemetryEventName;
payload: TypingsInstalledTelemetryEventPayload;
}
export interface TypingsInstalledTelemetryEventPayload {
/**
* Comma separated list of installed typing packages
*/
installedPackages: string;
}
export interface NavBarResponse extends Response {
body?: NavigationBarItem[];
}

View File

@ -1,4 +1,5 @@
/// <reference types="node" />
/// <reference path="shared.ts" />
/// <reference path="session.ts" />
// used in fs.writeSync
/* tslint:disable:no-null-keyword */
@ -17,7 +18,6 @@ namespace ts.server {
homedir(): string
} = require("os");
function getGlobalTypingsCacheLocation() {
let basePath: string;
switch (process.platform) {
@ -184,8 +184,10 @@ namespace ts.server {
private socket: NodeSocket;
private projectService: ProjectService;
private throttledOperations: ThrottledOperations;
private telemetrySender: EventSender;
constructor(
private readonly telemetryEnabled: boolean,
private readonly logger: server.Logger,
host: ServerHost,
eventPort: number,
@ -214,15 +216,22 @@ namespace ts.server {
this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8");
}
setTelemetrySender(telemetrySender: EventSender) {
this.telemetrySender = telemetrySender;
}
attach(projectService: ProjectService) {
this.projectService = projectService;
if (this.logger.hasLevel(LogLevel.requestTime)) {
this.logger.info("Binding...");
}
const args: string[] = ["--globalTypingsCacheLocation", this.globalTypingsCacheLocation];
const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation];
if (this.telemetryEnabled) {
args.push(Arguments.EnableTelemetry);
}
if (this.logger.loggingEnabled() && this.logger.getLogFileName()) {
args.push("--logFile", combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`));
args.push(Arguments.LogFile, combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`));
}
const execArgv: string[] = [];
{
@ -268,12 +277,25 @@ namespace ts.server {
});
}
private handleMessage(response: SetTypings | InvalidateCachedTypings) {
private handleMessage(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent) {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Received response: ${JSON.stringify(response)}`);
}
if (response.kind === EventInstall) {
if (this.telemetrySender) {
const body: protocol.TypingsInstalledTelemetryEventBody = {
telemetryEventName: "typingsInstalled",
payload: {
installedPackages: response.packagesToInstall.join(",")
}
};
const eventName: protocol.TelemetryEventName = "telemetry";
this.telemetrySender.event(body, eventName);
}
return;
}
this.projectService.updateTypingsForProject(response);
if (response.kind == "set" && this.socket) {
if (response.kind == ActionSet && this.socket) {
this.sendEvent(0, "setTypings", response);
}
}
@ -288,18 +310,25 @@ namespace ts.server {
useSingleInferredProject: boolean,
disableAutomaticTypingAcquisition: boolean,
globalTypingsCacheLocation: string,
telemetryEnabled: boolean,
logger: server.Logger) {
super(
host,
cancellationToken,
useSingleInferredProject,
disableAutomaticTypingAcquisition
? nullTypingsInstaller
: new NodeTypingsInstaller(logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine),
Buffer.byteLength,
process.hrtime,
logger,
canUseEvents);
const typingsInstaller = disableAutomaticTypingAcquisition
? undefined
: new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine);
super(
host,
cancellationToken,
useSingleInferredProject,
typingsInstaller || nullTypingsInstaller,
Buffer.byteLength,
process.hrtime,
logger,
canUseEvents);
if (telemetryEnabled && typingsInstaller) {
typingsInstaller.setTelemetrySender(this);
}
}
exit() {
@ -526,17 +555,17 @@ namespace ts.server {
let eventPort: number;
{
const index = sys.args.indexOf("--eventPort");
if (index >= 0 && index < sys.args.length - 1) {
const v = parseInt(sys.args[index + 1]);
if (!isNaN(v)) {
eventPort = v;
}
const str = findArgument("--eventPort");
const v = str && parseInt(str);
if (!isNaN(v)) {
eventPort = v;
}
}
const useSingleInferredProject = sys.args.indexOf("--useSingleInferredProject") >= 0;
const disableAutomaticTypingAcquisition = sys.args.indexOf("--disableAutomaticTypingAcquisition") >= 0;
const useSingleInferredProject = hasArgument("--useSingleInferredProject");
const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition");
const telemetryEnabled = hasArgument(Arguments.EnableTelemetry);
const ioSession = new IOSession(
sys,
cancellationToken,
@ -545,6 +574,7 @@ namespace ts.server {
useSingleInferredProject,
disableAutomaticTypingAcquisition,
getGlobalTypingsCacheLocation(),
telemetryEnabled,
logger);
process.on("uncaughtException", function (err: Error) {
ioSession.logError(err, "unknown");

View File

@ -73,6 +73,10 @@ namespace ts.server {
project: Project;
}
export interface EventSender {
event(payload: any, eventName: string): void;
}
function allEditsBeforePos(edits: ts.TextChange[], pos: number) {
for (const edit of edits) {
if (textSpanEnd(edit.span) >= pos) {
@ -165,7 +169,7 @@ namespace ts.server {
return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`;
}
export class Session {
export class Session implements EventSender {
private readonly gcTimer: GcTimer;
protected projectService: ProjectService;
private errorTimer: any; /*NodeJS.Timer | number*/

24
src/server/shared.ts Normal file
View File

@ -0,0 +1,24 @@
/// <reference path="types.d.ts" />
namespace ts.server {
export const ActionSet: ActionSet = "action::set";
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
export const EventInstall: EventInstall = "event::install";
export namespace Arguments {
export const GlobalCacheLocation = "--globalTypingsCacheLocation";
export const LogFile = "--logFile";
export const EnableTelemetry = "--enableTelemetry";
}
export function hasArgument(argumentName: string) {
return sys.args.indexOf(argumentName) >= 0;
}
export function findArgument(argumentName: string) {
const index = sys.args.indexOf(argumentName);
return index >= 0 && index < sys.args.length - 1
? sys.args[index + 1]
: undefined;
}
}

View File

@ -18,6 +18,7 @@
"files": [
"../services/shims.ts",
"../services/utilities.ts",
"shared.ts",
"utilities.ts",
"scriptVersionCache.ts",
"scriptInfo.ts",

View File

@ -15,6 +15,7 @@
"files": [
"../services/shims.ts",
"../services/utilities.ts",
"shared.ts",
"utilities.ts",
"scriptVersionCache.ts",
"scriptInfo.ts",

30
src/server/types.d.ts vendored
View File

@ -41,23 +41,37 @@ declare namespace ts.server {
readonly kind: "closeProject";
}
export type SetRequest = "set";
export type InvalidateRequest = "invalidate";
export type ActionSet = "action::set";
export type ActionInvalidate = "action::invalidate";
export type EventInstall = "event::install";
export interface TypingInstallerResponse {
readonly projectName: string;
readonly kind: SetRequest | InvalidateRequest;
readonly kind: ActionSet | ActionInvalidate | EventInstall;
}
export interface SetTypings extends TypingInstallerResponse {
export interface ProjectResponse extends TypingInstallerResponse {
readonly projectName: string;
}
export interface SetTypings extends ProjectResponse {
readonly typingOptions: ts.TypingOptions;
readonly compilerOptions: ts.CompilerOptions;
readonly typings: string[];
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly kind: SetRequest;
readonly kind: ActionSet;
}
export interface InvalidateCachedTypings extends TypingInstallerResponse {
readonly kind: InvalidateRequest;
export interface InvalidateCachedTypings extends ProjectResponse {
readonly kind: ActionInvalidate;
}
export interface InvalidateCachedTypings extends ProjectResponse {
readonly kind: ActionInvalidate;
}
export interface TypingsInstallEvent extends TypingInstallerResponse {
readonly packagesToInstall: ReadonlyArray<string>;
readonly kind: EventInstall;
}
export interface InstallTypingHost extends JsTyping.TypingResolutionHost {

View File

@ -75,12 +75,13 @@ namespace ts.server.typingsInstaller {
private readonly npmPath: string;
readonly typesRegistry: Map<void>;
constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) {
constructor(globalTypingsCacheLocation: string, throttleLimit: number, telemetryEnabled: boolean, log: Log) {
super(
sys,
globalTypingsCacheLocation,
toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
throttleLimit,
telemetryEnabled,
log);
if (this.log.isEnabled()) {
this.log.writeLine(`Process id: ${process.pid}`);
@ -145,15 +146,10 @@ namespace ts.server.typingsInstaller {
}
}
function findArgument(argumentName: string) {
const index = sys.args.indexOf(argumentName);
return index >= 0 && index < sys.args.length - 1
? sys.args[index + 1]
: undefined;
}
const logFilePath = findArgument(server.Arguments.LogFile);
const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation);
const telemetryEnabled = hasArgument(server.Arguments.EnableTelemetry);
const logFilePath = findArgument("--logFile");
const globalTypingsCacheLocation = findArgument("--globalTypingsCacheLocation");
const log = new FileLog(logFilePath);
if (log.isEnabled()) {
process.on("uncaughtException", (e: Error) => {
@ -166,6 +162,6 @@ namespace ts.server.typingsInstaller {
}
process.exit(0);
});
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log);
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, telemetryEnabled, log);
installer.listen();
}

View File

@ -17,6 +17,7 @@
},
"files": [
"../types.d.ts",
"../shared.ts",
"typingsInstaller.ts",
"nodeTypingsInstaller.ts"
]

View File

@ -2,6 +2,7 @@
/// <reference path="../../compiler/moduleNameResolver.ts" />
/// <reference path="../../services/jsTyping.ts"/>
/// <reference path="../types.d.ts"/>
/// <reference path="../shared.ts"/>
namespace ts.server.typingsInstaller {
interface NpmConfig {
@ -88,6 +89,7 @@ namespace ts.server.typingsInstaller {
readonly globalCachePath: string,
readonly safeListPath: Path,
readonly throttleLimit: number,
readonly telemetryEnabled: boolean,
protected readonly log = nullLog) {
if (this.log.isEnabled()) {
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`);
@ -298,9 +300,17 @@ namespace ts.server.typingsInstaller {
this.installRunCount++;
this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => {
if (this.telemetryEnabled) {
this.sendResponse(<TypingsInstallEvent>{
kind: EventInstall,
packagesToInstall: scopedTypings
});
}
if (!ok) {
return;
}
// TODO: watch project directory
if (this.log.isEnabled()) {
this.log.writeLine(`Requested to install typings ${JSON.stringify(scopedTypings)}, installed typings ${JSON.stringify(scopedTypings)}`);
@ -354,7 +364,7 @@ namespace ts.server.typingsInstaller {
this.log.writeLine(`Got FS notification for ${f}, handler is already invoked '${isInvoked}'`);
}
if (!isInvoked) {
this.sendResponse({ projectName: projectName, kind: "invalidate" });
this.sendResponse({ projectName: projectName, kind: server.ActionInvalidate });
isInvoked = true;
}
});
@ -370,7 +380,7 @@ namespace ts.server.typingsInstaller {
compilerOptions: request.compilerOptions,
typings,
unresolvedImports: request.unresolvedImports,
kind: "set"
kind: ActionSet
};
}
@ -392,6 +402,6 @@ namespace ts.server.typingsInstaller {
}
protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | TypingsInstallEvent): void;
}
}

View File

@ -1,4 +1,5 @@
/// <reference path="types.d.ts" />
/// <reference path="shared.ts" />
namespace ts.server {
export enum LogLevel {
@ -10,6 +11,7 @@ namespace ts.server {
export const emptyArray: ReadonlyArray<any> = [];
export interface Logger {
close(): void;
hasLevel(level: LogLevel): boolean;