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

@ -129,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([
"initializeTSConfig.ts",
"printer.ts",
"textChanges.ts",
"telemetry.ts",
"transform.ts",
"customTransforms.ts",
].map(function (f) {

View File

@ -692,8 +692,7 @@ namespace ts {
return typeAcquisition;
}
/* @internal */
export function getOptionNameMap(): OptionNameMap {
function getOptionNameMap(): OptionNameMap {
if (optionNameMapCache) {
return optionNameMapCache;
}
@ -746,7 +745,6 @@ namespace ts {
const options: CompilerOptions = {};
const fileNames: string[] = [];
const errors: Diagnostic[] = [];
const { optionNameMap, shortOptionNames } = getOptionNameMap();
parseStrings(commandLine);
return {
@ -758,21 +756,13 @@ namespace ts {
function parseStrings(args: string[]) {
let i = 0;
while (i < args.length) {
let s = args[i];
const s = args[i];
i++;
if (s.charCodeAt(0) === CharacterCodes.at) {
parseResponseFile(s.slice(1));
}
else if (s.charCodeAt(0) === CharacterCodes.minus) {
s = s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
// Try to translate short option names to their full equivalents.
const short = shortOptionNames.get(s);
if (short !== undefined) {
s = short;
}
const opt = optionNameMap.get(s);
const opt = getOptionFromName(s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true);
if (opt) {
if (opt.isTSConfigOnly) {
errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name));
@ -860,6 +850,19 @@ namespace ts {
}
}
function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined {
optionName = optionName.toLowerCase();
const { optionNameMap, shortOptionNames } = getOptionNameMap();
// Try to translate short option names to their full equivalents.
if (allowShort) {
const short = shortOptionNames.get(optionName);
if (short !== undefined) {
optionName = short;
}
}
return optionNameMap.get(optionName);
}
/**
* Read tsconfig.json file
* @param fileName The path to the config file
@ -1705,4 +1708,42 @@ namespace ts {
function caseInsensitiveKeyMapper(key: string) {
return key.toLowerCase();
}
/**
* Produces a cleaned version of compiler options with personally identifiying info (aka, paths) removed.
* Also converts enum values back to strings.
*/
/* @internal */
export function convertCompilerOptionsForTelemetry(opts: ts.CompilerOptions): ts.CompilerOptions {
const out: ts.CompilerOptions = {};
for (const key in opts) if (opts.hasOwnProperty(key)) {
const type = getOptionFromName(key);
if (type !== undefined) { // Ignore unknown options
out[key] = getOptionValueWithEmptyStrings(opts[key], type);
}
}
return out;
}
function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} {
switch (option.type) {
case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "".
return "";
case "string": // Could be any arbitrary string -- use empty string instead.
return "";
case "number": // Allow numbers, but be sure to check it's actually a number.
return typeof value === "number" ? value : "";
case "boolean":
return typeof value === "boolean" ? value : "";
case "list":
const elementType = (option as CommandLineOptionOfListType).element;
return ts.isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : "";
default:
return ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => {
if (optionEnumValue === value) {
return optionStringValue;
}
});
}
}
}

View File

@ -521,6 +521,18 @@ namespace ts {
return result || array;
}
export function mapDefined<T>(array: ReadonlyArray<T>, mapFn: (x: T, i: number) => T | undefined): ReadonlyArray<T> {
const result: T[] = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
const mapped = mapFn(item, i);
if (mapped !== undefined) {
result.push(mapped);
}
}
return result;
}
/**
* Computes the first matching span of elements and returns a tuple of the first span
* and the remaining elements.

View File

@ -127,6 +127,7 @@
"./unittests/printer.ts",
"./unittests/transform.ts",
"./unittests/customTransforms.ts",
"./unittests/textChanges.ts"
"./unittests/textChanges.ts",
"./unittests/telemetry.ts"
]
}

View File

@ -0,0 +1,291 @@
/// <reference path="../harness.ts" />
/// <reference path="./tsserverProjectSystem.ts" />
namespace ts.projectSystem {
describe("project telemetry", () => {
it("does nothing for inferred project", () => {
const file = makeFile("/a.js");
const et = new EventTracker([file]);
et.service.openClientFile(file.path);
assert.equal(et.getEvents().length, 0);
});
it("only sends an event once", () => {
const file = makeFile("/a.ts");
const tsconfig = makeFile("/tsconfig.json", {});
const et = new EventTracker([file, tsconfig]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({});
et.service.closeClientFile(file.path);
checkNumberOfProjects(et.service, { configuredProjects: 0 });
et.service.openClientFile(file.path);
checkNumberOfProjects(et.service, { configuredProjects: 1 });
assert.equal(et.getEvents().length, 0);
});
it("counts files by extension", () => {
const files = ["ts.ts", "tsx.tsx", "moo.ts", "dts.d.ts", "jsx.jsx", "js.js", "badExtension.badExtension"].map(f => makeFile(`/src/${f}`));
const notIncludedFile = makeFile("/bin/ts.js");
const compilerOptions: ts.CompilerOptions = { allowJs: true };
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] });
const et = new EventTracker([...files, notIncludedFile, tsconfig]);
et.service.openClientFile(files[0].path);
et.assertProjectInfoTelemetryEvent({
fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 },
compilerOptions,
include: true,
});
});
it("works with external project", () => {
const file1 = makeFile("/a.ts");
const et = new EventTracker([file1]);
const compilerOptions: ts.CompilerOptions = { strict: true };
const projectFileName = "foo.csproj";
open();
// TODO: Apparently compilerOptions is mutated, so have to repeat it here!
et.assertProjectInfoTelemetryEvent({
compilerOptions: { strict: true },
compileOnSave: true,
// These properties can't be present for an external project, so they are undefined instead of false.
extends: undefined,
files: undefined,
include: undefined,
exclude: undefined,
configFileName: "other",
projectType: "external",
});
// Also test that opening an external project only sends an event once.
et.service.closeExternalProject(projectFileName);
checkNumberOfProjects(et.service, { externalProjects: 0 });
open();
assert.equal(et.getEvents().length, 0);
function open(): void {
et.service.openExternalProject({
rootFiles: toExternalFiles([file1.path]),
options: compilerOptions,
projectFileName: projectFileName,
});
checkNumberOfProjects(et.service, { externalProjects: 1 });
}
});
it("does not expose paths", () => {
const file = makeFile("/a.ts");
const compilerOptions: ts.CompilerOptions = {
project: "",
outFile: "hunter2.js",
outDir: "hunter2",
rootDir: "hunter2",
baseUrl: "hunter2",
rootDirs: ["hunter2"],
typeRoots: ["hunter2"],
types: ["hunter2"],
sourceRoot: "hunter2",
mapRoot: "hunter2",
jsxFactory: "hunter2",
out: "hunter2",
reactNamespace: "hunter2",
charset: "hunter2",
locale: "hunter2",
declarationDir: "hunter2",
paths: {
"*": ["hunter2"],
},
// Boolean / number options get through
declaration: true,
// List of string enum gets through -- but only if legitimately a member of the enum
lib: ["es6", "dom", "hunter2"],
// Sensitive data doesn't get through even if sent to an option of safe type
checkJs: "hunter2" as any as boolean,
};
const safeCompilerOptions: ts.CompilerOptions = {
project: "",
outFile: "",
outDir: "",
rootDir: "",
baseUrl: "",
rootDirs: [""],
typeRoots: [""],
types: [""],
sourceRoot: "",
mapRoot: "",
jsxFactory: "",
out: "",
reactNamespace: "",
charset: "",
locale: "",
declarationDir: "",
paths: "" as any,
declaration: true,
lib: ["es6", "dom"],
checkJs: "" as any as boolean,
};
(compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored.
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] });
const et = new EventTracker([file, tsconfig]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
compilerOptions: safeCompilerOptions,
files: true,
});
});
it("sends telemetry for extends, files, include, exclude, and compileOnSave", () => {
const file = makeFile("/hunter2/a.ts");
const tsconfig = makeFile("/tsconfig.json", {
compilerOptions: {},
extends: "hunter2.json",
files: ["hunter2/a.ts"],
include: ["hunter2"],
exclude: ["hunter2"],
compileOnSave: true,
});
const et = new EventTracker([tsconfig, file]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
extends: true,
files: true,
include: true,
exclude: true,
compileOnSave: true,
});
});
const autoJsCompilerOptions = {
// Apparently some options are added by default.
allowJs: true,
allowSyntheticDefaultImports: true,
maxNodeModuleJsDepth: 2,
skipLibCheck: true,
};
it("sends telemetry for typeAcquisition settings", () => {
const file = makeFile("/a.js");
const jsconfig = makeFile("/jsconfig.json", {
compilerOptions: {},
typeAcquisition: {
enable: true,
enableAutoDiscovery: false,
include: ["hunter2", "hunter3"],
exclude: [],
},
});
const et = new EventTracker([jsconfig, file]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ js: 1 }),
compilerOptions: autoJsCompilerOptions,
typeAcquisition: {
enable: true,
include: true,
exclude: false,
},
configFileName: "jsconfig.json",
});
});
it("detects whether language service was disabled", () => {
const file = makeFile("/a.js");
const tsconfig = makeFile("/jsconfig.json", {});
const et = new EventTracker([tsconfig, file]);
et.host.getFileSize = () => server.maxProgramSizeForNonTsFiles + 1;
et.service.openClientFile(file.path);
et.getEvent<server.ProjectLanguageServiceStateEvent>(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ js: 1 }),
compilerOptions: autoJsCompilerOptions,
configFileName: "jsconfig.json",
typeAcquisition: {
enable: true,
include: false,
exclude: false,
},
languageServiceEnabled: false,
});
});
});
class EventTracker {
private events: server.ProjectServiceEvent[] = [];
readonly service: TestProjectService;
readonly host: projectSystem.TestServerHost;
constructor(files: projectSystem.FileOrFolder[]) {
this.host = createServerHost(files);
this.service = createProjectService(this.host, {
eventHandler: event => {
this.events.push(event);
},
});
}
getEvents(): ReadonlyArray<server.ProjectServiceEvent> {
const events = this.events;
this.events = [];
return events;
}
assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>): void {
assert.deepEqual(this.getEvent<server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), makePayload(partial));
}
getEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"], mayBeMore = false): T["data"] {
if (mayBeMore) assert(this.events.length !== 0); else assert.equal(this.events.length, 1);
const event = this.events.shift();
assert.equal(event.eventName, eventName);
return event.data;
}
}
function makePayload(partial: Partial<server.ProjectInfoTelemetryEventData>): server.ProjectInfoTelemetryEventData {
return {
fileStats: fileStats({ ts: 1 }),
compilerOptions: {},
extends: false,
files: false,
include: false,
exclude: false,
compileOnSave: false,
typeAcquisition: {
enable: false,
exclude: false,
include: false,
},
configFileName: "tsconfig.json",
projectType: "configured",
languageServiceEnabled: true,
version: ts.version,
...partial
};
}
function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder {
return { path, content: typeof content === "string" ? "" : JSON.stringify(content) };
}
function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats {
return { ts: 0, tsx: 0, dts: 0, js: 0, jsx: 0, ...nonZeroStats };
}
}

View File

@ -2234,7 +2234,7 @@ namespace ts.projectSystem {
let lastEvent: server.ProjectLanguageServiceStateEvent;
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent) {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
return;
}
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
@ -2284,7 +2284,7 @@ namespace ts.projectSystem {
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
let lastEvent: server.ProjectLanguageServiceStateEvent;
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
if (e.eventName === server.ConfigFileDiagEvent) {
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
return;
}
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);

View File

@ -44,7 +44,7 @@ namespace ts.projectSystem {
});
}
import typingsName = server.typingsInstaller.typingsName;
import typingsName = TI.typingsName;
describe("local module", () => {
it("should not be picked up", () => {
@ -73,7 +73,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { typesRegistry: createTypesRegistry("config"), globalTypingsCacheLocation: typesCache });
}
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) {
assert(false, "should not be called");
}
})();
@ -121,7 +121,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/jquery"];
const typingFiles = [jquery];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -165,7 +165,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { typesRegistry: createTypesRegistry("jquery") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/jquery"];
const typingFiles = [jquery];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -672,7 +672,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/jquery"];
const typingFiles = [jqueryDTS];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -718,7 +718,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/jquery"];
const typingFiles = [jqueryDTS];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -765,7 +765,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/jquery"];
const typingFiles = [jqueryDTS];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -808,7 +808,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/commander"];
const typingFiles = [commander];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -849,7 +849,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("node", "commander") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/node", "@types/commander"];
const typingFiles = [node, commander];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -888,7 +888,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("foo") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
executeCommand(this, host, ["foo"], [], cb);
}
})();
@ -996,7 +996,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) });
}
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) {
assert(false, "runCommand should not be invoked");
}
})();
@ -1060,7 +1060,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/commander"];
const typingFiles = [commander];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -1110,7 +1110,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
const installedTypings = ["@types/commander"];
const typingFiles = [commander];
executeCommand(this, host, installedTypings, typingFiles, cb);
@ -1157,7 +1157,7 @@ namespace ts.projectSystem {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
}
installWorker(_requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
executeCommand(this, host, "", [], cb);
}
sendResponse(response: server.SetTypings | server.InvalidateCachedTypings | server.BeginInstallTypes | server.EndInstallTypes) {

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
*/