From dd20a9ccdb5f56bf1f0f73d978fa996d6f9ad787 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 23 May 2017 14:38:57 -0700 Subject: [PATCH 01/11] Add project telemetry --- Jakefile.js | 1 + src/compiler/commandLineParser.ts | 72 ++++++-- src/compiler/core.ts | 12 ++ src/harness/tsconfig.json | 3 +- src/harness/unittests/telemetry.ts | 154 ++++++++++++++++++ .../unittests/tsserverProjectSystem.ts | 2 +- src/harness/unittests/typingsInstaller.ts | 28 ++-- src/server/editorServices.ts | 63 ++++++- src/server/project.ts | 13 ++ 9 files changed, 318 insertions(+), 30 deletions(-) create mode 100644 src/harness/unittests/telemetry.ts diff --git a/Jakefile.js b/Jakefile.js index 807546d59f87e..8cf6c9f595356 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/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9494098c072f3..082c7c3a69477 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 @@ -1704,4 +1707,47 @@ 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. + */ + 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) { + const value = getOptionValueExcludingPaths(opts[key], type); + if (value !== undefined) { + out[key] = value; + } + } + } + return out; + } + + function getOptionValueExcludingPaths(value: any, option: CommandLineOption): {} | undefined { + switch (option.type) { + case "object": // Don't allow "paths" + case "string": // Don't allow arbitrary strings. + return undefined; + case "number": // Allow numbers, but be sure to check it's actually a number. + return typeof value === "number" ? value : undefined; + case "boolean": + return typeof value === "boolean" ? value : undefined; + case "list": + const elementType = (option as CommandLineOptionOfListType).element; + if (elementType.type === "number" || elementType.type === "boolean" || typeof elementType.type !== "string") { + return mapDefined(value, v => getOptionValueExcludingPaths(v, elementType)); + } + return undefined; + default: + const stringValue = ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => { + if (optionEnumValue === value) { + return { value: optionStringValue }; + } + }); + return stringValue && stringValue.value; + } + } } \ No newline at end of file diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 52a7b5f5a7a1b..d1d21e9469498 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -521,6 +521,18 @@ namespace ts { return result || array; } + export function mapDefined(array: ReadonlyArray, mapFn: (x: T, i: number) => T | undefined): ReadonlyArray { + 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. diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index d752c0f235b7c..6553f3667a794 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 0000000000000..641013601cea0 --- /dev/null +++ b/src/harness/unittests/telemetry.ts @@ -0,0 +1,154 @@ +/// +/// + +namespace ts.projectSystem { + describe("project telemetry", () => { + it("does nothing for inferred project", () => { + const file = mkFile("/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 = mkFile("/a.ts"); + const tsconfig = mkFile("/tsconfig.json", {}); + + const et = new EventTracker([file, tsconfig]); + et.service.openClientFile(file.path); + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 1, js: 0, dts: 0 }, compilerOptions: {} }); + + 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 => mkFile(`/src/${f}`)); + const notIncludedFile = mkFile("/bin/ts.js"); + const compilerOptions: ts.CompilerOptions = { allowJs: true }; + const tsconfig = mkFile("/tsconfig.json", { compilerOptions, include: ["src"] }); + + const et = new EventTracker([...files, notIncludedFile, tsconfig]); + et.service.openClientFile(files[0].path); + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 3, js: 2, dts: 1 }, compilerOptions }); + }); + + it("works with external project", () => { + const file1 = mkFile("/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! + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 1, js: 0, dts: 0 }, compilerOptions: { strict: true } }); + + // 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 = mkFile("/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, + }; + (compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored. + const tsconfig = mkFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] }); + + const et = new EventTracker([file, tsconfig]); + et.service.openClientFile(file.path); + + assert.deepEqual(et.getProjectTelemetryEvent(), { + fileStats: { ts: 1, js: 0, dts: 0 }, + compilerOptions: { + declaration: true, + lib: ["es6", "dom"], + }, + }); + }); + }); + + class EventTracker { + private events: server.ProjectServiceEvent[] = []; + readonly service: TestProjectService; + + constructor(files: projectSystem.FileOrFolder[]) { + this.service = createProjectService(createServerHost(files), { + eventHandler: event => { + this.events.push(event); + }, + }); + } + + getEvents(): ReadonlyArray { + const events = this.events; + this.events = []; + return events; + } + + getProjectTelemetryEvent(): server.ProjectTelemetryEventData { + return this.getEvent(ts.server.ProjectTelemetryEvent); + } + + private getEvent(eventName: T["eventName"]): T["data"] { + const events = this.getEvents(); + assert.equal(events.length, 1); + const event = events[0]; + assert.equal(event.eventName, eventName); + return event.data; + } + } + + function mkFile(path: string, content: {} = ""): projectSystem.FileOrFolder { + return { path, content: typeof content === "string" ? "" : JSON.stringify(content) }; + } +} diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index efa9249030050..f3f04bf2b6236 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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.ProjectTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index af95874a32cc8..699b180742860 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 d8322f5e7c50c..0ef06b8d89756 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 ProjectTelemetryEvent = "telemetry"; export interface ContextEvent { eventName: typeof ContextEvent; @@ -29,7 +30,49 @@ namespace ts.server { data: { project: Project, languageServiceEnabled: boolean }; } - export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent; + export interface ProjectTelemetryEvent { + readonly eventName: typeof ProjectTelemetryEvent; + readonly data: ProjectTelemetryEventData; + } + + export interface FileStats { + readonly ts: number; + readonly js: number; + readonly dts: number; + } + + function getFileStats(fileNames: string[]): FileStats { + let ts = 0, js = 0, dts = 0; + for (const file of fileNames) { + switch (extensionFromPath(file)) { + case Extension.Ts: + case Extension.Tsx: + ts++; + break; + case Extension.Js: + case Extension.Jsx: + js++; + break; + case Extension.Dts: + dts++; + break; + default: Debug.fail(); + } + } + return { ts, js, dts }; + } + + export interface ProjectTelemetryEventData { + /** 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; + } + + export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectTelemetryEvent; export interface ProjectServiceEventHandler { (event: ProjectServiceEvent): void; @@ -345,6 +388,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; @@ -984,9 +1030,23 @@ 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: ts.server.ExternalProject | ts.server.ConfiguredProject): void { + if (this.seenProjects.has(projectKey)) { + return; + } + this.seenProjects.set(projectKey, true); + + const data: ProjectTelemetryEventData = { + fileStats: getFileStats(project.getFileNames()), + compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), + }; + this.eventHandler({ eventName: ProjectTelemetryEvent, data }); + } + private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) { if (!this.eventHandler) { return; @@ -1020,6 +1080,7 @@ namespace ts.server { project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); this.configuredProjects.push(project); + this.sendProjectTelemetry(project.getConfigFilePath(), project); return project; } diff --git a/src/server/project.ts b/src/server/project.ts index 6372d1f5c03b9..fbc04de9d3e32 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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, From 1e4798a9acac6759b12e0429e3f706e13d855cf0 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 07:29:50 -0700 Subject: [PATCH 02/11] Respond to some PR comments --- src/compiler/commandLineParser.ts | 1 + src/harness/unittests/telemetry.ts | 33 +++++++++++-------- .../unittests/tsserverProjectSystem.ts | 2 +- src/server/editorServices.ts | 33 +++++-------------- src/server/project.ts | 3 +- 5 files changed, 31 insertions(+), 41 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 082c7c3a69477..65688f7c97885 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1712,6 +1712,7 @@ namespace ts { * 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)) { diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 641013601cea0..49bb43fdc0c19 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -4,19 +4,19 @@ namespace ts.projectSystem { describe("project telemetry", () => { it("does nothing for inferred project", () => { - const file = mkFile("/a.js"); + 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 = mkFile("/a.ts"); - const tsconfig = mkFile("/tsconfig.json", {}); + const file = makeFile("/a.ts"); + const tsconfig = makeFile("/tsconfig.json", {}); const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 1, js: 0, dts: 0 }, compilerOptions: {} }); + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {} }); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -28,18 +28,18 @@ namespace ts.projectSystem { }); it("counts files by extension", () => { - const files = ["ts.ts", "tsx.tsx", "moo.ts", "dts.d.ts", "jsx.jsx", "js.js", "badExtension.badExtension"].map(f => mkFile(`/src/${f}`)); - const notIncludedFile = mkFile("/bin/ts.js"); + 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 = mkFile("/tsconfig.json", { compilerOptions, include: ["src"] }); + const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] }); const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 3, js: 2, dts: 1 }, compilerOptions }); + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions }); }); it("works with external project", () => { - const file1 = mkFile("/a.ts"); + const file1 = makeFile("/a.ts"); const et = new EventTracker([file1]); const compilerOptions: ts.CompilerOptions = { strict: true }; @@ -48,7 +48,7 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 1, js: 0, dts: 0 }, compilerOptions: { strict: true } }); + assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true } }); // Also test that opening an external project only sends an event once. @@ -69,7 +69,7 @@ namespace ts.projectSystem { }); it("Does not expose paths", () => { - const file = mkFile("/a.ts"); + const file = makeFile("/a.ts"); const compilerOptions: ts.CompilerOptions = { project: "", @@ -102,13 +102,13 @@ namespace ts.projectSystem { checkJs: "hunter2" as any as boolean, }; (compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored. - const tsconfig = mkFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] }); + const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] }); const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); assert.deepEqual(et.getProjectTelemetryEvent(), { - fileStats: { ts: 1, js: 0, dts: 0 }, + fileStats: fileStats({ ts: 1 }), compilerOptions: { declaration: true, lib: ["es6", "dom"], @@ -148,7 +148,12 @@ namespace ts.projectSystem { } } - function mkFile(path: string, content: {} = ""): projectSystem.FileOrFolder { + function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder { return { path, content: typeof content === "string" ? "" : JSON.stringify(content) }; } + + function fileStats(nonZeroStats: Partial) { + 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 f3f04bf2b6236..17dc06575e1be 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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.ProjectTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0ef06b8d89756..278976c5b2970 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -36,30 +36,11 @@ namespace ts.server { } export interface FileStats { - readonly ts: number; - readonly js: number; - readonly dts: number; - } - - function getFileStats(fileNames: string[]): FileStats { - let ts = 0, js = 0, dts = 0; - for (const file of fileNames) { - switch (extensionFromPath(file)) { - case Extension.Ts: - case Extension.Tsx: - ts++; - break; - case Extension.Js: - case Extension.Jsx: - js++; - break; - case Extension.Dts: - dts++; - break; - default: Debug.fail(); - } - } - return { ts, js, dts }; + js: number; + jsx: number; + ts: number; + tsx: number; + dts: number; } export interface ProjectTelemetryEventData { @@ -1040,8 +1021,10 @@ namespace ts.server { } this.seenProjects.set(projectKey, true); + if (!this.eventHandler) return; + const data: ProjectTelemetryEventData = { - fileStats: getFileStats(project.getFileNames()), + fileStats: countEachFileTypes(project.getScriptInfos()), compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), }; this.eventHandler({ eventName: ProjectTelemetryEvent, data }); diff --git a/src/server/project.ts b/src/server/project.ts index fbc04de9d3e32..cd462ab11e360 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -20,7 +20,8 @@ namespace ts.server { } } - 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) { From 4225162c2509c5f6d42ecb0ae84ff464abcfcc46 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 10:38:36 -0700 Subject: [PATCH 03/11] Wrap event in a TelemetryEvent payload --- src/harness/unittests/telemetry.ts | 12 +++---- .../unittests/tsserverProjectSystem.ts | 4 +-- src/server/editorServices.ts | 33 ++++++++++--------- src/server/session.ts | 11 ++++++- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 49bb43fdc0c19..23549a512bc58 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -16,7 +16,7 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {} }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {} }); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -35,7 +35,7 @@ namespace ts.projectSystem { const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions }); }); it("works with external project", () => { @@ -48,7 +48,7 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true } }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true } }); // Also test that opening an external project only sends an event once. @@ -107,7 +107,7 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectTelemetryEvent(), { + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { declaration: true, @@ -135,8 +135,8 @@ namespace ts.projectSystem { return events; } - getProjectTelemetryEvent(): server.ProjectTelemetryEventData { - return this.getEvent(ts.server.ProjectTelemetryEvent); + getProjectInfoTelemetryEvent(): server.ProjectInfoTelemetryEventData { + return this.getEvent(ts.server.ProjectInfoTelemetryEvent); } private getEvent(eventName: T["eventName"]): T["data"] { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 17dc06575e1be..6f898e7f4a66c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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 || e.eventName === server.ProjectTelemetryEvent) { + 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 || e.eventName === server.ProjectTelemetryEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 278976c5b2970..d7baf205eda2d 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -13,7 +13,7 @@ namespace ts.server { export const ContextEvent = "context"; export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; - export const ProjectTelemetryEvent = "telemetry"; + export const ProjectInfoTelemetryEvent = "projectInfo"; export interface ContextEvent { eventName: typeof ContextEvent; @@ -30,20 +30,13 @@ namespace ts.server { data: { project: Project, languageServiceEnabled: boolean }; } - export interface ProjectTelemetryEvent { - readonly eventName: typeof ProjectTelemetryEvent; - readonly data: ProjectTelemetryEventData; + /** 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 FileStats { - js: number; - jsx: number; - ts: number; - tsx: number; - dts: number; - } - - export interface ProjectTelemetryEventData { + export interface ProjectInfoTelemetryEventData { /** Count of file extensions seen in the project. */ readonly fileStats: FileStats; /** @@ -53,7 +46,15 @@ namespace ts.server { readonly compilerOptions: ts.CompilerOptions; } - export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectTelemetryEvent; + 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; @@ -1023,11 +1024,11 @@ namespace ts.server { if (!this.eventHandler) return; - const data: ProjectTelemetryEventData = { + const data: ProjectInfoTelemetryEventData = { fileStats: countEachFileTypes(project.getScriptInfos()), compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), }; - this.eventHandler({ eventName: ProjectTelemetryEvent, data }); + this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); } private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) { diff --git a/src/server/session.ts b/src/server/session.ts index 846607cd5caf1..6ec234952db52 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -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({ 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; + } } } From 2eb5c015f44995e8d95ce7ccbc3994a2cab2ae1e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 11:00:04 -0700 Subject: [PATCH 04/11] Replace paths with empty string instead of removing them entirely --- src/compiler/commandLineParser.ts | 30 +++++++++++----------------- src/harness/unittests/telemetry.ts | 32 +++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 65688f7c97885..0a1b590980652 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1717,38 +1717,32 @@ namespace ts { const out: ts.CompilerOptions = {}; for (const key in opts) if (opts.hasOwnProperty(key)) { const type = getOptionFromName(key); - if (type !== undefined) { - const value = getOptionValueExcludingPaths(opts[key], type); - if (value !== undefined) { - out[key] = value; - } + if (type !== undefined) { // Ignore unknown options + out[key] = getOptionValueWithEmptyStrings(opts[key], type); } } return out; } - function getOptionValueExcludingPaths(value: any, option: CommandLineOption): {} | undefined { + function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} { switch (option.type) { - case "object": // Don't allow "paths" - case "string": // Don't allow arbitrary strings. - return undefined; + 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 : undefined; + return typeof value === "number" ? value : ""; case "boolean": - return typeof value === "boolean" ? value : undefined; + return typeof value === "boolean" ? value : ""; case "list": const elementType = (option as CommandLineOptionOfListType).element; - if (elementType.type === "number" || elementType.type === "boolean" || typeof elementType.type !== "string") { - return mapDefined(value, v => getOptionValueExcludingPaths(v, elementType)); - } - return undefined; + return ts.isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; default: - const stringValue = ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => { + return ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => { if (optionEnumValue === value) { - return { value: optionStringValue }; + return optionStringValue; } }); - return stringValue && stringValue.value; } } } \ No newline at end of file diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 23549a512bc58..7885c734042b2 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -68,7 +68,7 @@ namespace ts.projectSystem { } }); - it("Does not expose paths", () => { + it("does not expose paths", () => { const file = makeFile("/a.ts"); const compilerOptions: ts.CompilerOptions = { @@ -101,6 +101,31 @@ namespace ts.projectSystem { // 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"] }); @@ -109,10 +134,7 @@ namespace ts.projectSystem { assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), - compilerOptions: { - declaration: true, - lib: ["es6", "dom"], - }, + compilerOptions: safeCompilerOptions, }); }); }); From c2c71dacd832551ddbd867de93c594fcc249213a Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 11:03:12 -0700 Subject: [PATCH 05/11] Add "version" property to payload --- src/harness/unittests/telemetry.ts | 7 ++++--- src/server/editorServices.ts | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 7885c734042b2..2979302bfff3f 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -16,7 +16,7 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {} }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {}, version: ts.version }); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -35,7 +35,7 @@ namespace ts.projectSystem { const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, version: ts.version }); }); it("works with external project", () => { @@ -48,7 +48,7 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true } }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true }, version: ts.version }); // Also test that opening an external project only sends an event once. @@ -135,6 +135,7 @@ namespace ts.projectSystem { assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: safeCompilerOptions, + version: ts.version, }); }); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d7baf205eda2d..74b3a554e456f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -44,6 +44,8 @@ namespace ts.server { * Enum compiler options will be converted to strings. */ readonly compilerOptions: ts.CompilerOptions; + /** TypeScript version used by the server. */ + readonly version: string; } export interface FileStats { @@ -1027,6 +1029,7 @@ namespace ts.server { const data: ProjectInfoTelemetryEventData = { fileStats: countEachFileTypes(project.getScriptInfos()), compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), + version: ts.version, }; this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); } From 318f19ac7dfb1bbad1505207ea55e36d1d7fc6a9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 11:27:44 -0700 Subject: [PATCH 06/11] Add telemetry for typeAcquisition settings --- src/harness/unittests/telemetry.ts | 61 ++++++++++++++++++++++++++++-- src/server/editorServices.ts | 18 +++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 2979302bfff3f..f93275d89d8f8 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -10,13 +10,24 @@ namespace ts.projectSystem { assert.equal(et.getEvents().length, 0); }); + const typeAcquisition: server.ProjectInfoTypeAcquisitionData = { + enable: false, + exclude: [], + include: [], + }; + 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); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: {}, version: ts.version }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + typeAcquisition, + version: ts.version, + }); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -35,7 +46,12 @@ namespace ts.projectSystem { const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, version: ts.version }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, + compilerOptions, + typeAcquisition, + version: ts.version, + }); }); it("works with external project", () => { @@ -48,7 +64,12 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: { strict: true }, version: ts.version }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + fileStats: fileStats({ ts: 1 }), + compilerOptions: { strict: true }, + typeAcquisition, + version: ts.version, + }); // Also test that opening an external project only sends an event once. @@ -132,9 +153,41 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { fileStats: fileStats({ ts: 1 }), compilerOptions: safeCompilerOptions, + typeAcquisition, + version: ts.version, + }); + }); + + 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); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + fileStats: fileStats({ js: 1 }), + compilerOptions: { + // Apparently some options are added by default. + allowJs: true, + allowSyntheticDefaultImports: true, + maxNodeModuleJsDepth: 2, + skipLibCheck: true, + }, + typeAcquisition: { + enable: true, + include: ["", ""], + exclude: [], + }, version: ts.version, }); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 74b3a554e456f..4b77b025b04b4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -44,10 +44,18 @@ namespace ts.server { * Enum compiler options will be converted to strings. */ readonly compilerOptions: ts.CompilerOptions; + readonly typeAcquisition: ProjectInfoTypeAcquisitionData; /** 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: Array<"">; + readonly exclude: Array<"">; + } + export interface FileStats { readonly js: number; readonly jsx: number; @@ -1029,9 +1037,19 @@ namespace ts.server { const data: ProjectInfoTelemetryEventData = { fileStats: countEachFileTypes(project.getScriptInfos()), compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), + typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()), version: ts.version, }; this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); + + /** Need to blank out paths. */ + function convertTypeAcquisition(ta: TypeAcquisition): ProjectInfoTypeAcquisitionData { + return { + enable: ta.enable, + include: ta.include.map<"">(() => ""), + exclude: ta.exclude.map<"">(() => ""), + }; + } } private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) { From 0ebac8f4c73aeafcf0d16a0113bd33c81c70ade8 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 24 May 2017 15:25:07 -0700 Subject: [PATCH 07/11] Add "files", "include", "exclude", and "compileOnSave" --- src/harness/unittests/telemetry.ts | 85 +++++++++++++++++++----------- src/server/editorServices.ts | 20 +++++-- src/server/utilities.ts | 4 +- 3 files changed, 73 insertions(+), 36 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index f93275d89d8f8..51063a7d88308 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -9,25 +9,13 @@ namespace ts.projectSystem { et.service.openClientFile(file.path); assert.equal(et.getEvents().length, 0); }); - - const typeAcquisition: server.ProjectInfoTypeAcquisitionData = { - enable: false, - exclude: [], - include: [], - }; - 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); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { - fileStats: fileStats({ ts: 1 }), - compilerOptions: {}, - typeAcquisition, - version: ts.version, - }); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({})); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -46,12 +34,11 @@ namespace ts.projectSystem { const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, - typeAcquisition, - version: ts.version, - }); + include: true, + })); }); it("works with external project", () => { @@ -64,12 +51,14 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { - fileStats: fileStats({ ts: 1 }), + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ compilerOptions: { strict: true }, - typeAcquisition, - version: ts.version, - }); + compileOnSave: true, + // These properties can't be present for an external project, so they are undefined instead of false. + files: undefined, + include: undefined, + exclude: undefined, + })); // Also test that opening an external project only sends an event once. @@ -153,12 +142,30 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { - fileStats: fileStats({ ts: 1 }), + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ compilerOptions: safeCompilerOptions, - typeAcquisition, - version: ts.version, + files: true, + })); + }); + + it("sends telemetry for files, include, exclude, and compileOnSave", () => { + const file = makeFile("/hunter2/a.ts"); + const tsconfig = makeFile("/tsconfig.json", { + compilerOptions: {}, + files: ["hunter2/a.ts"], + include: ["hunter2"], + exclude: ["hunter2"], + compileOnSave: true, }); + + const et = new EventTracker([tsconfig, file]); + et.service.openClientFile(file.path); + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + files: true, + include: true, + exclude: true, + compileOnSave: true, + })); }); it("sends telemetry for typeAcquisition settings", () => { @@ -174,7 +181,7 @@ namespace ts.projectSystem { }); const et = new EventTracker([jsconfig, file]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), { + assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ fileStats: fileStats({ js: 1 }), compilerOptions: { // Apparently some options are added by default. @@ -188,8 +195,7 @@ namespace ts.projectSystem { include: ["", ""], exclude: [], }, - version: ts.version, - }); + })); }); }); @@ -224,12 +230,29 @@ namespace ts.projectSystem { } } + function makePayload(partial: Partial): server.ProjectInfoTelemetryEventData { + return { + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + files: false, + include: false, + exclude: false, + compileOnSave: false, + typeAcquisition: { + enable: false, + exclude: [], + include: [], + }, + version: ts.version, + ...partial + }; + } + function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder { return { path, content: typeof content === "string" ? "" : JSON.stringify(content) }; } - function fileStats(nonZeroStats: Partial) { + function fileStats(nonZeroStats: Partial): server.FileStats { return { ts: 0, tsx: 0, dts: 0, js: 0, jsx: 0, ...nonZeroStats }; } - } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4b77b025b04b4..359ce85fd82e8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -44,6 +44,12 @@ namespace ts.server { * Enum compiler options will be converted to strings. */ readonly compilerOptions: ts.CompilerOptions; + // "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 files: boolean | undefined; + readonly include: boolean | undefined; + readonly exclude: boolean | undefined; + readonly compileOnSave: boolean; readonly typeAcquisition: ProjectInfoTypeAcquisitionData; /** TypeScript version used by the server. */ readonly version: string; @@ -972,7 +978,9 @@ namespace ts.server { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, compilerOptions: parsedCommandLine.options, - configHasFilesProperty: config["files"] !== undefined, + configHasFilesProperty: config.files !== undefined, + configHasIncludeProperty: config.include !== undefined, + configHasExcludeProperty: config.exclude !== undefined, wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories), typeAcquisition: parsedCommandLine.typeAcquisition, compileOnSave: parsedCommandLine.compileOnSave @@ -1026,7 +1034,7 @@ namespace ts.server { return project; } - private sendProjectTelemetry(projectKey: string, project: ts.server.ExternalProject | ts.server.ConfiguredProject): void { + private sendProjectTelemetry(projectKey: string, project: ts.server.ExternalProject | ts.server.ConfiguredProject, projectOptions?: ProjectOptions): void { if (this.seenProjects.has(projectKey)) { return; } @@ -1038,6 +1046,10 @@ namespace ts.server { fileStats: countEachFileTypes(project.getScriptInfos()), compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()), + files: projectOptions && projectOptions.configHasFilesProperty, + include: projectOptions && projectOptions.configHasIncludeProperty, + exclude: projectOptions && projectOptions.configHasExcludeProperty, + compileOnSave: project.compileOnSaveEnabled, version: ts.version, }; this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); @@ -1085,7 +1097,7 @@ namespace ts.server { project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); this.configuredProjects.push(project); - this.sendProjectTelemetry(project.getConfigFilePath(), project); + this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } @@ -1118,7 +1130,7 @@ namespace ts.server { const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName); const projectOptions: ProjectOptions = conversionResult.success ? conversionResult.projectOptions - : { files: [], compilerOptions: {}, typeAcquisition: { enable: false } }; + : { files: [], compilerOptions: {}, 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/utilities.ts b/src/server/utilities.ts index ffc09f29ccdea..9e6b0bc416b93 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -167,7 +167,9 @@ namespace ts.server { /** * true if config file explicitly listed files */ - configHasFilesProperty?: boolean; + configHasFilesProperty: boolean; + configHasIncludeProperty: boolean; + configHasExcludeProperty: boolean; /** * these fields can be present in the project file */ From 6ad3baa194fc69f3f71a77146e9c68e1bedbbed8 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 25 May 2017 07:20:13 -0700 Subject: [PATCH 08/11] Convert typingsOptions include and exclude to booleanss --- src/harness/unittests/telemetry.ts | 8 ++++---- src/server/editorServices.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 51063a7d88308..7ba30d4e032e2 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -192,8 +192,8 @@ namespace ts.projectSystem { }, typeAcquisition: { enable: true, - include: ["", ""], - exclude: [], + include: true, + exclude: false, }, })); }); @@ -240,8 +240,8 @@ namespace ts.projectSystem { compileOnSave: false, typeAcquisition: { enable: false, - exclude: [], - include: [], + exclude: false, + include: false, }, version: ts.version, ...partial diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 359ce85fd82e8..1dec645562842 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -58,8 +58,8 @@ namespace ts.server { export interface ProjectInfoTypeAcquisitionData { readonly enable: boolean; // Actual values of include/exclude entries are scrubbed. - readonly include: Array<"">; - readonly exclude: Array<"">; + readonly include: boolean; + readonly exclude: boolean; } export interface FileStats { @@ -1055,11 +1055,11 @@ namespace ts.server { this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); /** Need to blank out paths. */ - function convertTypeAcquisition(ta: TypeAcquisition): ProjectInfoTypeAcquisitionData { + function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { return { - enable: ta.enable, - include: ta.include.map<"">(() => ""), - exclude: ta.exclude.map<"">(() => ""), + enable, + include: include.length !== 0, + exclude: exclude.length !== 0, }; } } From e6401d7a17a6b14f5ad320716961018f8023bfe0 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 25 May 2017 11:52:17 -0700 Subject: [PATCH 09/11] Add "extends", "configFileName", and "projectType" --- src/harness/unittests/telemetry.ts | 37 ++++++++++++++++++++---------- src/server/editorServices.ts | 30 +++++++++++++++++++----- src/server/utilities.ts | 1 + 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 7ba30d4e032e2..a2610fe544aa8 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -15,7 +15,7 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({})); + et.assertProjectInfoTelemetryEvent({}); et.service.closeClientFile(file.path); checkNumberOfProjects(et.service, { configuredProjects: 0 }); @@ -34,11 +34,11 @@ namespace ts.projectSystem { const et = new EventTracker([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + et.assertProjectInfoTelemetryEvent({ fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, include: true, - })); + }); }); it("works with external project", () => { @@ -51,14 +51,17 @@ namespace ts.projectSystem { open(); // TODO: Apparently compilerOptions is mutated, so have to repeat it here! - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + 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: undefined, + projectType: "external", + }); // Also test that opening an external project only sends an event once. @@ -142,16 +145,17 @@ namespace ts.projectSystem { const et = new EventTracker([file, tsconfig]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + et.assertProjectInfoTelemetryEvent({ compilerOptions: safeCompilerOptions, files: true, - })); + }); }); - it("sends telemetry for files, include, exclude, and compileOnSave", () => { + 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"], @@ -160,12 +164,13 @@ namespace ts.projectSystem { const et = new EventTracker([tsconfig, file]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + et.assertProjectInfoTelemetryEvent({ + extends: true, files: true, include: true, exclude: true, compileOnSave: true, - })); + }); }); it("sends telemetry for typeAcquisition settings", () => { @@ -181,7 +186,7 @@ namespace ts.projectSystem { }); const et = new EventTracker([jsconfig, file]); et.service.openClientFile(file.path); - assert.deepEqual(et.getProjectInfoTelemetryEvent(), makePayload({ + et.assertProjectInfoTelemetryEvent({ fileStats: fileStats({ js: 1 }), compilerOptions: { // Apparently some options are added by default. @@ -195,7 +200,8 @@ namespace ts.projectSystem { include: true, exclude: false, }, - })); + configFileName: "jsconfig.json", + }); }); }); @@ -217,6 +223,10 @@ namespace ts.projectSystem { return events; } + assertProjectInfoTelemetryEvent(partial: Partial): void { + assert.deepEqual(this.getProjectInfoTelemetryEvent(), makePayload(partial)); + } + getProjectInfoTelemetryEvent(): server.ProjectInfoTelemetryEventData { return this.getEvent(ts.server.ProjectInfoTelemetryEvent); } @@ -234,6 +244,7 @@ namespace ts.projectSystem { return { fileStats: fileStats({ ts: 1 }), compilerOptions: {}, + extends: false, files: false, include: false, exclude: false, @@ -243,6 +254,8 @@ namespace ts.projectSystem { exclude: false, include: false, }, + configFileName: "tsconfig.json", + projectType: "configured", version: ts.version, ...partial }; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1dec645562842..ca07afe402fed 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -44,13 +44,18 @@ namespace ts.server { * Enum compiler options will be converted to strings. */ readonly compilerOptions: ts.CompilerOptions; - // "files", "include", or "exclude" will be undefined if an external config is used. + // "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; + + /** Is 'undefined' if config file name is not tsconfig or jsconfig. */ + readonly configFileName: "tsconfig.json" | "jsconfig.json" | undefined; + readonly projectType: "external" | "configured"; /** TypeScript version used by the server. */ readonly version: string; } @@ -978,6 +983,7 @@ namespace ts.server { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, compilerOptions: parsedCommandLine.options, + configHasExtendsProperty: config.extends !== undefined, configHasFilesProperty: config.files !== undefined, configHasIncludeProperty: config.include !== undefined, configHasExcludeProperty: config.exclude !== undefined, @@ -1034,7 +1040,7 @@ namespace ts.server { return project; } - private sendProjectTelemetry(projectKey: string, project: ts.server.ExternalProject | ts.server.ConfiguredProject, projectOptions?: ProjectOptions): void { + private sendProjectTelemetry(projectKey: string, project: server.ExternalProject | server.ConfiguredProject, projectOptions?: ProjectOptions): void { if (this.seenProjects.has(projectKey)) { return; } @@ -1046,20 +1052,32 @@ namespace ts.server { 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", version: ts.version, }; this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data }); - /** Need to blank out paths. */ + function configFileName(): ProjectInfoTelemetryEventData["configFileName"] { + if (!(project instanceof server.ConfiguredProject)) { + return undefined; + } + + const configFilePath = project instanceof server.ConfiguredProject && project.getConfigFilePath(); + const base = ts.getBaseFileName(configFilePath); + return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined; + } + function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { return { enable, - include: include.length !== 0, - exclude: exclude.length !== 0, + include: include !== undefined && include.length !== 0, + exclude: exclude !== undefined && exclude.length !== 0, }; } } @@ -1130,7 +1148,7 @@ namespace ts.server { const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName); const projectOptions: ProjectOptions = conversionResult.success ? conversionResult.projectOptions - : { files: [], compilerOptions: {}, configHasFilesProperty: false, configHasIncludeProperty: false, configHasExcludeProperty: false, 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/utilities.ts b/src/server/utilities.ts index 9e6b0bc416b93..093958b60c55f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -164,6 +164,7 @@ namespace ts.server { } export interface ProjectOptions { + configHasExtendsProperty: boolean; /** * true if config file explicitly listed files */ From 0500cf7bfc66fe57f4043e3ce8c3a4b553d0cdae Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 25 May 2017 12:31:51 -0700 Subject: [PATCH 10/11] configFileName: Use "other" instead of undefined --- src/harness/unittests/telemetry.ts | 2 +- src/server/editorServices.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index a2610fe544aa8..ff9540461872f 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -59,7 +59,7 @@ namespace ts.projectSystem { files: undefined, include: undefined, exclude: undefined, - configFileName: undefined, + configFileName: "other", projectType: "external", }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ca07afe402fed..b802faf19ece0 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -53,8 +53,7 @@ namespace ts.server { readonly compileOnSave: boolean; readonly typeAcquisition: ProjectInfoTypeAcquisitionData; - /** Is 'undefined' if config file name is not tsconfig or jsconfig. */ - readonly configFileName: "tsconfig.json" | "jsconfig.json" | undefined; + readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other"; readonly projectType: "external" | "configured"; /** TypeScript version used by the server. */ readonly version: string; @@ -1065,12 +1064,12 @@ namespace ts.server { function configFileName(): ProjectInfoTelemetryEventData["configFileName"] { if (!(project instanceof server.ConfiguredProject)) { - return undefined; + return "other"; } const configFilePath = project instanceof server.ConfiguredProject && project.getConfigFilePath(); const base = ts.getBaseFileName(configFilePath); - return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined; + return base === "tsconfig.json" || base === "jsconfig.json" ? base : "other"; } function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { From 970fe98abea7e552e3f2f5c3bd538b062c3a6e52 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 25 May 2017 12:56:01 -0700 Subject: [PATCH 11/11] Add "languageServiceEnabled" telemetry --- src/harness/unittests/telemetry.ts | 54 ++++++++++++++++++++---------- src/server/editorServices.ts | 2 ++ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index ff9540461872f..d3811edf25196 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -173,6 +173,14 @@ namespace ts.projectSystem { }); }); + 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", { @@ -188,13 +196,7 @@ namespace ts.projectSystem { et.service.openClientFile(file.path); et.assertProjectInfoTelemetryEvent({ fileStats: fileStats({ js: 1 }), - compilerOptions: { - // Apparently some options are added by default. - allowJs: true, - allowSyntheticDefaultImports: true, - maxNodeModuleJsDepth: 2, - skipLibCheck: true, - }, + compilerOptions: autoJsCompilerOptions, typeAcquisition: { enable: true, include: true, @@ -203,14 +205,36 @@ namespace ts.projectSystem { 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.service = createProjectService(createServerHost(files), { + this.host = createServerHost(files); + this.service = createProjectService(this.host, { eventHandler: event => { this.events.push(event); }, @@ -224,17 +248,12 @@ namespace ts.projectSystem { } assertProjectInfoTelemetryEvent(partial: Partial): void { - assert.deepEqual(this.getProjectInfoTelemetryEvent(), makePayload(partial)); - } - - getProjectInfoTelemetryEvent(): server.ProjectInfoTelemetryEventData { - return this.getEvent(ts.server.ProjectInfoTelemetryEvent); + assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), makePayload(partial)); } - private getEvent(eventName: T["eventName"]): T["data"] { - const events = this.getEvents(); - assert.equal(events.length, 1); - const event = events[0]; + 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; } @@ -256,6 +275,7 @@ namespace ts.projectSystem { }, configFileName: "tsconfig.json", projectType: "configured", + languageServiceEnabled: true, version: ts.version, ...partial }; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b802faf19ece0..7c0e0a0fb9202 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -55,6 +55,7 @@ namespace ts.server { readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other"; readonly projectType: "external" | "configured"; + readonly languageServiceEnabled: boolean; /** TypeScript version used by the server. */ readonly version: string; } @@ -1058,6 +1059,7 @@ namespace ts.server { compileOnSave: project.compileOnSaveEnabled, configFileName: configFileName(), projectType: project instanceof server.ExternalProject ? "external" : "configured", + languageServiceEnabled: project.languageServiceEnabled, version: ts.version, }; this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data });