Add project telemetry (#16050)

* Add project telemetry

* Respond to some PR comments

* Wrap event in a TelemetryEvent payload

* Replace paths with empty string instead of removing them entirely

* Add "version" property to payload

* Add telemetry for typeAcquisition settings

* Add "files", "include", "exclude", and "compileOnSave"

* Convert typingsOptions include and exclude to booleanss

* Add "extends", "configFileName", and "projectType"

* configFileName: Use "other" instead of undefined

* Add "languageServiceEnabled" telemetry
This commit is contained in:
Andy
2017-05-25 13:30:27 -07:00
committed by GitHub
parent 7cca4ba536
commit d052bb83ca
11 changed files with 505 additions and 36 deletions

View File

@@ -13,6 +13,7 @@ namespace ts.server {
export const ContextEvent = "context";
export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export const ProjectInfoTelemetryEvent = "projectInfo";
export interface ContextEvent {
eventName: typeof ContextEvent;
@@ -29,7 +30,52 @@ namespace ts.server {
data: { project: Project, languageServiceEnabled: boolean };
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent;
/** This will be converted to the payload of a protocol.TelemetryEvent in session.defaultEventHandler. */
export interface ProjectInfoTelemetryEvent {
readonly eventName: typeof ProjectInfoTelemetryEvent;
readonly data: ProjectInfoTelemetryEventData;
}
export interface ProjectInfoTelemetryEventData {
/** Count of file extensions seen in the project. */
readonly fileStats: FileStats;
/**
* Any compiler options that might contain paths will be taken out.
* Enum compiler options will be converted to strings.
*/
readonly compilerOptions: ts.CompilerOptions;
// "extends", "files", "include", or "exclude" will be undefined if an external config is used.
// Otherwise, we will use "true" if the property is present and "false" if it is missing.
readonly extends: boolean | undefined;
readonly files: boolean | undefined;
readonly include: boolean | undefined;
readonly exclude: boolean | undefined;
readonly compileOnSave: boolean;
readonly typeAcquisition: ProjectInfoTypeAcquisitionData;
readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other";
readonly projectType: "external" | "configured";
readonly languageServiceEnabled: boolean;
/** TypeScript version used by the server. */
readonly version: string;
}
export interface ProjectInfoTypeAcquisitionData {
readonly enable: boolean;
// Actual values of include/exclude entries are scrubbed.
readonly include: boolean;
readonly exclude: boolean;
}
export interface FileStats {
readonly js: number;
readonly jsx: number;
readonly ts: number;
readonly tsx: number;
readonly dts: number;
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
export interface ProjectServiceEventHandler {
(event: ProjectServiceEvent): void;
@@ -345,6 +391,9 @@ namespace ts.server {
public readonly pluginProbeLocations: ReadonlyArray<string>;
public readonly allowLocalPluginLoads: boolean;
/** Tracks projects that we have already sent telemetry for. */
private readonly seenProjects = createMap<true>();
constructor(opts: ProjectServiceOptions) {
this.host = opts.host;
this.logger = opts.logger;
@@ -934,7 +983,10 @@ namespace ts.server {
const projectOptions: ProjectOptions = {
files: parsedCommandLine.fileNames,
compilerOptions: parsedCommandLine.options,
configHasFilesProperty: config["files"] !== undefined,
configHasExtendsProperty: config.extends !== undefined,
configHasFilesProperty: config.files !== undefined,
configHasIncludeProperty: config.include !== undefined,
configHasExcludeProperty: config.exclude !== undefined,
wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories),
typeAcquisition: parsedCommandLine.typeAcquisition,
compileOnSave: parsedCommandLine.compileOnSave
@@ -984,9 +1036,53 @@ namespace ts.server {
this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined);
this.externalProjects.push(project);
this.sendProjectTelemetry(project.externalProjectName, project);
return project;
}
private sendProjectTelemetry(projectKey: string, project: server.ExternalProject | server.ConfiguredProject, projectOptions?: ProjectOptions): void {
if (this.seenProjects.has(projectKey)) {
return;
}
this.seenProjects.set(projectKey, true);
if (!this.eventHandler) return;
const data: ProjectInfoTelemetryEventData = {
fileStats: countEachFileTypes(project.getScriptInfos()),
compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()),
typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()),
extends: projectOptions && projectOptions.configHasExtendsProperty,
files: projectOptions && projectOptions.configHasFilesProperty,
include: projectOptions && projectOptions.configHasIncludeProperty,
exclude: projectOptions && projectOptions.configHasExcludeProperty,
compileOnSave: project.compileOnSaveEnabled,
configFileName: configFileName(),
projectType: project instanceof server.ExternalProject ? "external" : "configured",
languageServiceEnabled: project.languageServiceEnabled,
version: ts.version,
};
this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data });
function configFileName(): ProjectInfoTelemetryEventData["configFileName"] {
if (!(project instanceof server.ConfiguredProject)) {
return "other";
}
const configFilePath = project instanceof server.ConfiguredProject && project.getConfigFilePath();
const base = ts.getBaseFileName(configFilePath);
return base === "tsconfig.json" || base === "jsconfig.json" ? base : "other";
}
function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData {
return {
enable,
include: include !== undefined && include.length !== 0,
exclude: exclude !== undefined && exclude.length !== 0,
};
}
}
private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) {
if (!this.eventHandler) {
return;
@@ -1020,6 +1116,7 @@ namespace ts.server {
project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path));
this.configuredProjects.push(project);
this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions);
return project;
}
@@ -1052,7 +1149,7 @@ namespace ts.server {
const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName);
const projectOptions: ProjectOptions = conversionResult.success
? conversionResult.projectOptions
: { files: [], compilerOptions: {}, typeAcquisition: { enable: false } };
: { files: [], compilerOptions: {}, configHasExtendsProperty: false, configHasFilesProperty: false, configHasIncludeProperty: false, configHasExcludeProperty: false, typeAcquisition: { enable: false } };
const project = this.createAndAddConfiguredProject(configFileName, projectOptions, conversionResult.configFileErrors, clientFileName);
return {
success: conversionResult.success,

View File

@@ -13,7 +13,8 @@ namespace ts.server {
External
}
function countEachFileTypes(infos: ScriptInfo[]): { js: number, jsx: number, ts: number, tsx: number, dts: number } {
/* @internal */
export function countEachFileTypes(infos: ScriptInfo[]): FileStats {
const result = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 };
for (const info of infos) {
switch (info.scriptKind) {
@@ -730,6 +731,10 @@ namespace ts.server {
}
}
/**
* If a file is opened and no tsconfig (or jsconfig) is found,
* the file and its imports/references are put into an InferredProject.
*/
export class InferredProject extends Project {
private static newName = (() => {
@@ -823,6 +828,11 @@ namespace ts.server {
}
}
/**
* If a file is opened, the server will look for a tsconfig (or jsconfig)
* and if successfull create a ConfiguredProject for it.
* Otherwise it will create an InferredProject.
*/
export class ConfiguredProject extends Project {
private typeAcquisition: TypeAcquisition;
private projectFileWatcher: FileWatcher;
@@ -1048,6 +1058,10 @@ namespace ts.server {
}
}
/**
* Project whose configuration is handled externally, such as in a '.csproj'.
* These are created only if a host explicitly calls `openExternalProject`.
*/
export class ExternalProject extends Project {
private typeAcquisition: TypeAcquisition;
constructor(public externalProjectName: string,

View File

@@ -337,13 +337,22 @@ namespace ts.server {
const { triggerFile, configFileName, diagnostics } = event.data;
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
break;
case ProjectLanguageServiceStateEvent:
case ProjectLanguageServiceStateEvent: {
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
this.event<protocol.ProjectLanguageServiceStateEventBody>({
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled
}, eventName);
break;
}
case ProjectInfoTelemetryEvent: {
const eventName: protocol.TelemetryEventName = "telemetry";
this.event<protocol.TelemetryEventBody>({
telemetryEventName: event.eventName,
payload: event.data,
}, eventName);
break;
}
}
}

View File

@@ -164,10 +164,13 @@ namespace ts.server {
}
export interface ProjectOptions {
configHasExtendsProperty: boolean;
/**
* true if config file explicitly listed files
*/
configHasFilesProperty?: boolean;
configHasFilesProperty: boolean;
configHasIncludeProperty: boolean;
configHasExcludeProperty: boolean;
/**
* these fields can be present in the project file
*/