diff --git a/Jakefile.js b/Jakefile.js
index 807546d59f8..8cf6c9f5953 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -129,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([
"initializeTSConfig.ts",
"printer.ts",
"textChanges.ts",
+ "telemetry.ts",
"transform.ts",
"customTransforms.ts",
].map(function (f) {
diff --git a/package.json b/package.json
index 689c769a870..52531aa024b 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"travis-fold": "latest",
"ts-node": "latest",
"tslint": "next",
- "typescript": "next"
+ "typescript": "^2.3.3"
},
"scripts": {
"pretest": "jake tests",
diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts
index a5990a5a31b..347d632bef4 100644
--- a/src/compiler/commandLineParser.ts
+++ b/src/compiler/commandLineParser.ts
@@ -691,8 +691,7 @@ namespace ts {
return typeAcquisition;
}
- /* @internal */
- export function getOptionNameMap(): OptionNameMap {
+ function getOptionNameMap(): OptionNameMap {
if (optionNameMapCache) {
return optionNameMapCache;
}
@@ -745,7 +744,6 @@ namespace ts {
const options: CompilerOptions = {};
const fileNames: string[] = [];
const errors: Diagnostic[] = [];
- const { optionNameMap, shortOptionNames } = getOptionNameMap();
parseStrings(commandLine);
return {
@@ -757,21 +755,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));
@@ -859,6 +849,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
@@ -1661,4 +1664,42 @@ namespace ts {
function caseInsensitiveKeyMapper(key: string) {
return key.toLowerCase();
}
-}
\ No newline at end of file
+
+ /**
+ * 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;
+ }
+ });
+ }
+ }
+}
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index d752c0f235b..6553f3667a7 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -127,6 +127,7 @@
"./unittests/printer.ts",
"./unittests/transform.ts",
"./unittests/customTransforms.ts",
- "./unittests/textChanges.ts"
+ "./unittests/textChanges.ts",
+ "./unittests/telemetry.ts"
]
}
diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts
new file mode 100644
index 00000000000..d3811edf251
--- /dev/null
+++ b/src/harness/unittests/telemetry.ts
@@ -0,0 +1,291 @@
+///
+///
+
+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, /*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 {
+ const events = this.events;
+ this.events = [];
+ return events;
+ }
+
+ assertProjectInfoTelemetryEvent(partial: Partial): void {
+ assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), makePayload(partial));
+ }
+
+ getEvent(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 {
+ 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 {
+ return { ts: 0, tsx: 0, dts: 0, js: 0, jsx: 0, ...nonZeroStats };
+ }
+}
diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts
index 33838b5805f..57ce680faf1 100644
--- a/src/harness/unittests/tsserverProjectSystem.ts
+++ b/src/harness/unittests/tsserverProjectSystem.ts
@@ -2230,7 +2230,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);
@@ -2280,7 +2280,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);
diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts
index af95874a32c..699b1807428 100644
--- a/src/harness/unittests/typingsInstaller.ts
+++ b/src/harness/unittests/typingsInstaller.ts
@@ -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) {
diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts
index d8322f5e7c5..7c0e0a0fb92 100644
--- a/src/server/editorServices.ts
+++ b/src/server/editorServices.ts
@@ -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;
public readonly allowLocalPluginLoads: boolean;
+ /** Tracks projects that we have already sent telemetry for. */
+ private readonly seenProjects = createMap();
+
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,
diff --git a/src/server/project.ts b/src/server/project.ts
index 6aded117599..ac56a6e3b98 100644
--- a/src/server/project.ts
+++ b/src/server/project.ts
@@ -20,7 +20,7 @@ namespace ts.server {
}
}
- function countEachFileTypes(infos: ScriptInfo[]): { js: number, jsx: number, ts: number, tsx: number, dts: number } {
+ 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) {
@@ -737,6 +737,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 = (() => {
@@ -830,6 +834,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;
@@ -1055,6 +1064,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,
diff --git a/src/server/session.ts b/src/server/session.ts
index cee9698d6d9..96aaefbcdda 100644
--- a/src/server/session.ts
+++ b/src/server/session.ts
@@ -464,13 +464,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({
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled
}, eventName);
break;
+ }
+ case ProjectInfoTelemetryEvent: {
+ const eventName: protocol.TelemetryEventName = "telemetry";
+ this.event({
+ telemetryEventName: event.eventName,
+ payload: event.data,
+ }, eventName);
+ break;
+ }
}
}
diff --git a/src/server/utilities.ts b/src/server/utilities.ts
index ffc09f29ccd..093958b60c5 100644
--- a/src/server/utilities.ts
+++ b/src/server/utilities.ts
@@ -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
*/