mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-12 20:25:48 -06:00
Add project telemetry (#16050)
* Add project telemetry * Respond to some PR comments * Wrap event in a TelemetryEvent payload * Replace paths with empty string instead of removing them entirely * Add "version" property to payload * Add telemetry for typeAcquisition settings * Add "files", "include", "exclude", and "compileOnSave" * Convert typingsOptions include and exclude to booleanss * Add "extends", "configFileName", and "projectType" * configFileName: Use "other" instead of undefined * Add "languageServiceEnabled" telemetry
This commit is contained in:
parent
7cca4ba536
commit
d052bb83ca
@ -129,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([
|
||||
"initializeTSConfig.ts",
|
||||
"printer.ts",
|
||||
"textChanges.ts",
|
||||
"telemetry.ts",
|
||||
"transform.ts",
|
||||
"customTransforms.ts",
|
||||
].map(function (f) {
|
||||
|
||||
@ -692,8 +692,7 @@ namespace ts {
|
||||
return typeAcquisition;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function getOptionNameMap(): OptionNameMap {
|
||||
function getOptionNameMap(): OptionNameMap {
|
||||
if (optionNameMapCache) {
|
||||
return optionNameMapCache;
|
||||
}
|
||||
@ -746,7 +745,6 @@ namespace ts {
|
||||
const options: CompilerOptions = {};
|
||||
const fileNames: string[] = [];
|
||||
const errors: Diagnostic[] = [];
|
||||
const { optionNameMap, shortOptionNames } = getOptionNameMap();
|
||||
|
||||
parseStrings(commandLine);
|
||||
return {
|
||||
@ -758,21 +756,13 @@ namespace ts {
|
||||
function parseStrings(args: string[]) {
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
let s = args[i];
|
||||
const s = args[i];
|
||||
i++;
|
||||
if (s.charCodeAt(0) === CharacterCodes.at) {
|
||||
parseResponseFile(s.slice(1));
|
||||
}
|
||||
else if (s.charCodeAt(0) === CharacterCodes.minus) {
|
||||
s = s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase();
|
||||
|
||||
// Try to translate short option names to their full equivalents.
|
||||
const short = shortOptionNames.get(s);
|
||||
if (short !== undefined) {
|
||||
s = short;
|
||||
}
|
||||
|
||||
const opt = optionNameMap.get(s);
|
||||
const opt = getOptionFromName(s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true);
|
||||
if (opt) {
|
||||
if (opt.isTSConfigOnly) {
|
||||
errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name));
|
||||
@ -860,6 +850,19 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined {
|
||||
optionName = optionName.toLowerCase();
|
||||
const { optionNameMap, shortOptionNames } = getOptionNameMap();
|
||||
// Try to translate short option names to their full equivalents.
|
||||
if (allowShort) {
|
||||
const short = shortOptionNames.get(optionName);
|
||||
if (short !== undefined) {
|
||||
optionName = short;
|
||||
}
|
||||
}
|
||||
return optionNameMap.get(optionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read tsconfig.json file
|
||||
* @param fileName The path to the config file
|
||||
@ -1705,4 +1708,42 @@ namespace ts {
|
||||
function caseInsensitiveKeyMapper(key: string) {
|
||||
return key.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a cleaned version of compiler options with personally identifiying info (aka, paths) removed.
|
||||
* Also converts enum values back to strings.
|
||||
*/
|
||||
/* @internal */
|
||||
export function convertCompilerOptionsForTelemetry(opts: ts.CompilerOptions): ts.CompilerOptions {
|
||||
const out: ts.CompilerOptions = {};
|
||||
for (const key in opts) if (opts.hasOwnProperty(key)) {
|
||||
const type = getOptionFromName(key);
|
||||
if (type !== undefined) { // Ignore unknown options
|
||||
out[key] = getOptionValueWithEmptyStrings(opts[key], type);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} {
|
||||
switch (option.type) {
|
||||
case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "".
|
||||
return "";
|
||||
case "string": // Could be any arbitrary string -- use empty string instead.
|
||||
return "";
|
||||
case "number": // Allow numbers, but be sure to check it's actually a number.
|
||||
return typeof value === "number" ? value : "";
|
||||
case "boolean":
|
||||
return typeof value === "boolean" ? value : "";
|
||||
case "list":
|
||||
const elementType = (option as CommandLineOptionOfListType).element;
|
||||
return ts.isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : "";
|
||||
default:
|
||||
return ts.forEachEntry(option.type, (optionEnumValue, optionStringValue) => {
|
||||
if (optionEnumValue === value) {
|
||||
return optionStringValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,6 +521,18 @@ namespace ts {
|
||||
return result || array;
|
||||
}
|
||||
|
||||
export function mapDefined<T>(array: ReadonlyArray<T>, mapFn: (x: T, i: number) => T | undefined): ReadonlyArray<T> {
|
||||
const result: T[] = [];
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
const mapped = mapFn(item, i);
|
||||
if (mapped !== undefined) {
|
||||
result.push(mapped);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the first matching span of elements and returns a tuple of the first span
|
||||
* and the remaining elements.
|
||||
|
||||
@ -127,6 +127,7 @@
|
||||
"./unittests/printer.ts",
|
||||
"./unittests/transform.ts",
|
||||
"./unittests/customTransforms.ts",
|
||||
"./unittests/textChanges.ts"
|
||||
"./unittests/textChanges.ts",
|
||||
"./unittests/telemetry.ts"
|
||||
]
|
||||
}
|
||||
|
||||
291
src/harness/unittests/telemetry.ts
Normal file
291
src/harness/unittests/telemetry.ts
Normal file
@ -0,0 +1,291 @@
|
||||
/// <reference path="../harness.ts" />
|
||||
/// <reference path="./tsserverProjectSystem.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
describe("project telemetry", () => {
|
||||
it("does nothing for inferred project", () => {
|
||||
const file = makeFile("/a.js");
|
||||
const et = new EventTracker([file]);
|
||||
et.service.openClientFile(file.path);
|
||||
assert.equal(et.getEvents().length, 0);
|
||||
});
|
||||
it("only sends an event once", () => {
|
||||
const file = makeFile("/a.ts");
|
||||
const tsconfig = makeFile("/tsconfig.json", {});
|
||||
|
||||
const et = new EventTracker([file, tsconfig]);
|
||||
et.service.openClientFile(file.path);
|
||||
et.assertProjectInfoTelemetryEvent({});
|
||||
|
||||
et.service.closeClientFile(file.path);
|
||||
checkNumberOfProjects(et.service, { configuredProjects: 0 });
|
||||
|
||||
et.service.openClientFile(file.path);
|
||||
checkNumberOfProjects(et.service, { configuredProjects: 1 });
|
||||
|
||||
assert.equal(et.getEvents().length, 0);
|
||||
});
|
||||
|
||||
it("counts files by extension", () => {
|
||||
const files = ["ts.ts", "tsx.tsx", "moo.ts", "dts.d.ts", "jsx.jsx", "js.js", "badExtension.badExtension"].map(f => makeFile(`/src/${f}`));
|
||||
const notIncludedFile = makeFile("/bin/ts.js");
|
||||
const compilerOptions: ts.CompilerOptions = { allowJs: true };
|
||||
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] });
|
||||
|
||||
const et = new EventTracker([...files, notIncludedFile, tsconfig]);
|
||||
et.service.openClientFile(files[0].path);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 },
|
||||
compilerOptions,
|
||||
include: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("works with external project", () => {
|
||||
const file1 = makeFile("/a.ts");
|
||||
const et = new EventTracker([file1]);
|
||||
const compilerOptions: ts.CompilerOptions = { strict: true };
|
||||
|
||||
const projectFileName = "foo.csproj";
|
||||
|
||||
open();
|
||||
|
||||
// TODO: Apparently compilerOptions is mutated, so have to repeat it here!
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
compilerOptions: { strict: true },
|
||||
compileOnSave: true,
|
||||
// These properties can't be present for an external project, so they are undefined instead of false.
|
||||
extends: undefined,
|
||||
files: undefined,
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
configFileName: "other",
|
||||
projectType: "external",
|
||||
});
|
||||
|
||||
// Also test that opening an external project only sends an event once.
|
||||
|
||||
et.service.closeExternalProject(projectFileName);
|
||||
checkNumberOfProjects(et.service, { externalProjects: 0 });
|
||||
|
||||
open();
|
||||
assert.equal(et.getEvents().length, 0);
|
||||
|
||||
function open(): void {
|
||||
et.service.openExternalProject({
|
||||
rootFiles: toExternalFiles([file1.path]),
|
||||
options: compilerOptions,
|
||||
projectFileName: projectFileName,
|
||||
});
|
||||
checkNumberOfProjects(et.service, { externalProjects: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose paths", () => {
|
||||
const file = makeFile("/a.ts");
|
||||
|
||||
const compilerOptions: ts.CompilerOptions = {
|
||||
project: "",
|
||||
outFile: "hunter2.js",
|
||||
outDir: "hunter2",
|
||||
rootDir: "hunter2",
|
||||
baseUrl: "hunter2",
|
||||
rootDirs: ["hunter2"],
|
||||
typeRoots: ["hunter2"],
|
||||
types: ["hunter2"],
|
||||
sourceRoot: "hunter2",
|
||||
mapRoot: "hunter2",
|
||||
jsxFactory: "hunter2",
|
||||
out: "hunter2",
|
||||
reactNamespace: "hunter2",
|
||||
charset: "hunter2",
|
||||
locale: "hunter2",
|
||||
declarationDir: "hunter2",
|
||||
paths: {
|
||||
"*": ["hunter2"],
|
||||
},
|
||||
|
||||
// Boolean / number options get through
|
||||
declaration: true,
|
||||
|
||||
// List of string enum gets through -- but only if legitimately a member of the enum
|
||||
lib: ["es6", "dom", "hunter2"],
|
||||
|
||||
// Sensitive data doesn't get through even if sent to an option of safe type
|
||||
checkJs: "hunter2" as any as boolean,
|
||||
};
|
||||
const safeCompilerOptions: ts.CompilerOptions = {
|
||||
project: "",
|
||||
outFile: "",
|
||||
outDir: "",
|
||||
rootDir: "",
|
||||
baseUrl: "",
|
||||
rootDirs: [""],
|
||||
typeRoots: [""],
|
||||
types: [""],
|
||||
sourceRoot: "",
|
||||
mapRoot: "",
|
||||
jsxFactory: "",
|
||||
out: "",
|
||||
reactNamespace: "",
|
||||
charset: "",
|
||||
locale: "",
|
||||
declarationDir: "",
|
||||
paths: "" as any,
|
||||
|
||||
declaration: true,
|
||||
|
||||
lib: ["es6", "dom"],
|
||||
|
||||
checkJs: "" as any as boolean,
|
||||
};
|
||||
(compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored.
|
||||
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] });
|
||||
|
||||
const et = new EventTracker([file, tsconfig]);
|
||||
et.service.openClientFile(file.path);
|
||||
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
compilerOptions: safeCompilerOptions,
|
||||
files: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("sends telemetry for extends, files, include, exclude, and compileOnSave", () => {
|
||||
const file = makeFile("/hunter2/a.ts");
|
||||
const tsconfig = makeFile("/tsconfig.json", {
|
||||
compilerOptions: {},
|
||||
extends: "hunter2.json",
|
||||
files: ["hunter2/a.ts"],
|
||||
include: ["hunter2"],
|
||||
exclude: ["hunter2"],
|
||||
compileOnSave: true,
|
||||
});
|
||||
|
||||
const et = new EventTracker([tsconfig, file]);
|
||||
et.service.openClientFile(file.path);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
extends: true,
|
||||
files: true,
|
||||
include: true,
|
||||
exclude: true,
|
||||
compileOnSave: true,
|
||||
});
|
||||
});
|
||||
|
||||
const autoJsCompilerOptions = {
|
||||
// Apparently some options are added by default.
|
||||
allowJs: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
maxNodeModuleJsDepth: 2,
|
||||
skipLibCheck: true,
|
||||
};
|
||||
|
||||
it("sends telemetry for typeAcquisition settings", () => {
|
||||
const file = makeFile("/a.js");
|
||||
const jsconfig = makeFile("/jsconfig.json", {
|
||||
compilerOptions: {},
|
||||
typeAcquisition: {
|
||||
enable: true,
|
||||
enableAutoDiscovery: false,
|
||||
include: ["hunter2", "hunter3"],
|
||||
exclude: [],
|
||||
},
|
||||
});
|
||||
const et = new EventTracker([jsconfig, file]);
|
||||
et.service.openClientFile(file.path);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
fileStats: fileStats({ js: 1 }),
|
||||
compilerOptions: autoJsCompilerOptions,
|
||||
typeAcquisition: {
|
||||
enable: true,
|
||||
include: true,
|
||||
exclude: false,
|
||||
},
|
||||
configFileName: "jsconfig.json",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects whether language service was disabled", () => {
|
||||
const file = makeFile("/a.js");
|
||||
const tsconfig = makeFile("/jsconfig.json", {});
|
||||
const et = new EventTracker([tsconfig, file]);
|
||||
et.host.getFileSize = () => server.maxProgramSizeForNonTsFiles + 1;
|
||||
et.service.openClientFile(file.path);
|
||||
et.getEvent<server.ProjectLanguageServiceStateEvent>(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true);
|
||||
et.assertProjectInfoTelemetryEvent({
|
||||
fileStats: fileStats({ js: 1 }),
|
||||
compilerOptions: autoJsCompilerOptions,
|
||||
configFileName: "jsconfig.json",
|
||||
typeAcquisition: {
|
||||
enable: true,
|
||||
include: false,
|
||||
exclude: false,
|
||||
},
|
||||
languageServiceEnabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class EventTracker {
|
||||
private events: server.ProjectServiceEvent[] = [];
|
||||
readonly service: TestProjectService;
|
||||
readonly host: projectSystem.TestServerHost;
|
||||
|
||||
constructor(files: projectSystem.FileOrFolder[]) {
|
||||
this.host = createServerHost(files);
|
||||
this.service = createProjectService(this.host, {
|
||||
eventHandler: event => {
|
||||
this.events.push(event);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getEvents(): ReadonlyArray<server.ProjectServiceEvent> {
|
||||
const events = this.events;
|
||||
this.events = [];
|
||||
return events;
|
||||
}
|
||||
|
||||
assertProjectInfoTelemetryEvent(partial: Partial<server.ProjectInfoTelemetryEventData>): void {
|
||||
assert.deepEqual(this.getEvent<server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), makePayload(partial));
|
||||
}
|
||||
|
||||
getEvent<T extends server.ProjectServiceEvent>(eventName: T["eventName"], mayBeMore = false): T["data"] {
|
||||
if (mayBeMore) assert(this.events.length !== 0); else assert.equal(this.events.length, 1);
|
||||
const event = this.events.shift();
|
||||
assert.equal(event.eventName, eventName);
|
||||
return event.data;
|
||||
}
|
||||
}
|
||||
|
||||
function makePayload(partial: Partial<server.ProjectInfoTelemetryEventData>): server.ProjectInfoTelemetryEventData {
|
||||
return {
|
||||
fileStats: fileStats({ ts: 1 }),
|
||||
compilerOptions: {},
|
||||
extends: false,
|
||||
files: false,
|
||||
include: false,
|
||||
exclude: false,
|
||||
compileOnSave: false,
|
||||
typeAcquisition: {
|
||||
enable: false,
|
||||
exclude: false,
|
||||
include: false,
|
||||
},
|
||||
configFileName: "tsconfig.json",
|
||||
projectType: "configured",
|
||||
languageServiceEnabled: true,
|
||||
version: ts.version,
|
||||
...partial
|
||||
};
|
||||
}
|
||||
|
||||
function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder {
|
||||
return { path, content: typeof content === "string" ? "" : JSON.stringify(content) };
|
||||
}
|
||||
|
||||
function fileStats(nonZeroStats: Partial<server.FileStats>): server.FileStats {
|
||||
return { ts: 0, tsx: 0, dts: 0, js: 0, jsx: 0, ...nonZeroStats };
|
||||
}
|
||||
}
|
||||
@ -2234,7 +2234,7 @@ namespace ts.projectSystem {
|
||||
|
||||
let lastEvent: server.ProjectLanguageServiceStateEvent;
|
||||
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
|
||||
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent) {
|
||||
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
|
||||
return;
|
||||
}
|
||||
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
|
||||
@ -2284,7 +2284,7 @@ namespace ts.projectSystem {
|
||||
filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
|
||||
let lastEvent: server.ProjectLanguageServiceStateEvent;
|
||||
const session = createSession(host, /*typingsInstaller*/ undefined, e => {
|
||||
if (e.eventName === server.ConfigFileDiagEvent) {
|
||||
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
|
||||
return;
|
||||
}
|
||||
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -13,6 +13,7 @@ namespace ts.server {
|
||||
export const ContextEvent = "context";
|
||||
export const ConfigFileDiagEvent = "configFileDiag";
|
||||
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
|
||||
export const ProjectInfoTelemetryEvent = "projectInfo";
|
||||
|
||||
export interface ContextEvent {
|
||||
eventName: typeof ContextEvent;
|
||||
@ -29,7 +30,52 @@ namespace ts.server {
|
||||
data: { project: Project, languageServiceEnabled: boolean };
|
||||
}
|
||||
|
||||
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent;
|
||||
/** This will be converted to the payload of a protocol.TelemetryEvent in session.defaultEventHandler. */
|
||||
export interface ProjectInfoTelemetryEvent {
|
||||
readonly eventName: typeof ProjectInfoTelemetryEvent;
|
||||
readonly data: ProjectInfoTelemetryEventData;
|
||||
}
|
||||
|
||||
export interface ProjectInfoTelemetryEventData {
|
||||
/** Count of file extensions seen in the project. */
|
||||
readonly fileStats: FileStats;
|
||||
/**
|
||||
* Any compiler options that might contain paths will be taken out.
|
||||
* Enum compiler options will be converted to strings.
|
||||
*/
|
||||
readonly compilerOptions: ts.CompilerOptions;
|
||||
// "extends", "files", "include", or "exclude" will be undefined if an external config is used.
|
||||
// Otherwise, we will use "true" if the property is present and "false" if it is missing.
|
||||
readonly extends: boolean | undefined;
|
||||
readonly files: boolean | undefined;
|
||||
readonly include: boolean | undefined;
|
||||
readonly exclude: boolean | undefined;
|
||||
readonly compileOnSave: boolean;
|
||||
readonly typeAcquisition: ProjectInfoTypeAcquisitionData;
|
||||
|
||||
readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other";
|
||||
readonly projectType: "external" | "configured";
|
||||
readonly languageServiceEnabled: boolean;
|
||||
/** TypeScript version used by the server. */
|
||||
readonly version: string;
|
||||
}
|
||||
|
||||
export interface ProjectInfoTypeAcquisitionData {
|
||||
readonly enable: boolean;
|
||||
// Actual values of include/exclude entries are scrubbed.
|
||||
readonly include: boolean;
|
||||
readonly exclude: boolean;
|
||||
}
|
||||
|
||||
export interface FileStats {
|
||||
readonly js: number;
|
||||
readonly jsx: number;
|
||||
readonly ts: number;
|
||||
readonly tsx: number;
|
||||
readonly dts: number;
|
||||
}
|
||||
|
||||
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
|
||||
|
||||
export interface ProjectServiceEventHandler {
|
||||
(event: ProjectServiceEvent): void;
|
||||
@ -345,6 +391,9 @@ namespace ts.server {
|
||||
public readonly pluginProbeLocations: ReadonlyArray<string>;
|
||||
public readonly allowLocalPluginLoads: boolean;
|
||||
|
||||
/** Tracks projects that we have already sent telemetry for. */
|
||||
private readonly seenProjects = createMap<true>();
|
||||
|
||||
constructor(opts: ProjectServiceOptions) {
|
||||
this.host = opts.host;
|
||||
this.logger = opts.logger;
|
||||
@ -934,7 +983,10 @@ namespace ts.server {
|
||||
const projectOptions: ProjectOptions = {
|
||||
files: parsedCommandLine.fileNames,
|
||||
compilerOptions: parsedCommandLine.options,
|
||||
configHasFilesProperty: config["files"] !== undefined,
|
||||
configHasExtendsProperty: config.extends !== undefined,
|
||||
configHasFilesProperty: config.files !== undefined,
|
||||
configHasIncludeProperty: config.include !== undefined,
|
||||
configHasExcludeProperty: config.exclude !== undefined,
|
||||
wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories),
|
||||
typeAcquisition: parsedCommandLine.typeAcquisition,
|
||||
compileOnSave: parsedCommandLine.compileOnSave
|
||||
@ -984,9 +1036,53 @@ namespace ts.server {
|
||||
|
||||
this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined);
|
||||
this.externalProjects.push(project);
|
||||
this.sendProjectTelemetry(project.externalProjectName, project);
|
||||
return project;
|
||||
}
|
||||
|
||||
private sendProjectTelemetry(projectKey: string, project: server.ExternalProject | server.ConfiguredProject, projectOptions?: ProjectOptions): void {
|
||||
if (this.seenProjects.has(projectKey)) {
|
||||
return;
|
||||
}
|
||||
this.seenProjects.set(projectKey, true);
|
||||
|
||||
if (!this.eventHandler) return;
|
||||
|
||||
const data: ProjectInfoTelemetryEventData = {
|
||||
fileStats: countEachFileTypes(project.getScriptInfos()),
|
||||
compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()),
|
||||
typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()),
|
||||
extends: projectOptions && projectOptions.configHasExtendsProperty,
|
||||
files: projectOptions && projectOptions.configHasFilesProperty,
|
||||
include: projectOptions && projectOptions.configHasIncludeProperty,
|
||||
exclude: projectOptions && projectOptions.configHasExcludeProperty,
|
||||
compileOnSave: project.compileOnSaveEnabled,
|
||||
configFileName: configFileName(),
|
||||
projectType: project instanceof server.ExternalProject ? "external" : "configured",
|
||||
languageServiceEnabled: project.languageServiceEnabled,
|
||||
version: ts.version,
|
||||
};
|
||||
this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data });
|
||||
|
||||
function configFileName(): ProjectInfoTelemetryEventData["configFileName"] {
|
||||
if (!(project instanceof server.ConfiguredProject)) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
const configFilePath = project instanceof server.ConfiguredProject && project.getConfigFilePath();
|
||||
const base = ts.getBaseFileName(configFilePath);
|
||||
return base === "tsconfig.json" || base === "jsconfig.json" ? base : "other";
|
||||
}
|
||||
|
||||
function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData {
|
||||
return {
|
||||
enable,
|
||||
include: include !== undefined && include.length !== 0,
|
||||
exclude: exclude !== undefined && exclude.length !== 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) {
|
||||
if (!this.eventHandler) {
|
||||
return;
|
||||
@ -1020,6 +1116,7 @@ namespace ts.server {
|
||||
project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path));
|
||||
|
||||
this.configuredProjects.push(project);
|
||||
this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions);
|
||||
return project;
|
||||
}
|
||||
|
||||
@ -1052,7 +1149,7 @@ namespace ts.server {
|
||||
const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName);
|
||||
const projectOptions: ProjectOptions = conversionResult.success
|
||||
? conversionResult.projectOptions
|
||||
: { files: [], compilerOptions: {}, typeAcquisition: { enable: false } };
|
||||
: { files: [], compilerOptions: {}, configHasExtendsProperty: false, configHasFilesProperty: false, configHasIncludeProperty: false, configHasExcludeProperty: false, typeAcquisition: { enable: false } };
|
||||
const project = this.createAndAddConfiguredProject(configFileName, projectOptions, conversionResult.configFileErrors, clientFileName);
|
||||
return {
|
||||
success: conversionResult.success,
|
||||
|
||||
@ -13,7 +13,8 @@ namespace ts.server {
|
||||
External
|
||||
}
|
||||
|
||||
function countEachFileTypes(infos: ScriptInfo[]): { js: number, jsx: number, ts: number, tsx: number, dts: number } {
|
||||
/* @internal */
|
||||
export function countEachFileTypes(infos: ScriptInfo[]): FileStats {
|
||||
const result = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 };
|
||||
for (const info of infos) {
|
||||
switch (info.scriptKind) {
|
||||
@ -730,6 +731,10 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a file is opened and no tsconfig (or jsconfig) is found,
|
||||
* the file and its imports/references are put into an InferredProject.
|
||||
*/
|
||||
export class InferredProject extends Project {
|
||||
|
||||
private static newName = (() => {
|
||||
@ -823,6 +828,11 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a file is opened, the server will look for a tsconfig (or jsconfig)
|
||||
* and if successfull create a ConfiguredProject for it.
|
||||
* Otherwise it will create an InferredProject.
|
||||
*/
|
||||
export class ConfiguredProject extends Project {
|
||||
private typeAcquisition: TypeAcquisition;
|
||||
private projectFileWatcher: FileWatcher;
|
||||
@ -1048,6 +1058,10 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project whose configuration is handled externally, such as in a '.csproj'.
|
||||
* These are created only if a host explicitly calls `openExternalProject`.
|
||||
*/
|
||||
export class ExternalProject extends Project {
|
||||
private typeAcquisition: TypeAcquisition;
|
||||
constructor(public externalProjectName: string,
|
||||
|
||||
@ -337,13 +337,22 @@ namespace ts.server {
|
||||
const { triggerFile, configFileName, diagnostics } = event.data;
|
||||
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
|
||||
break;
|
||||
case ProjectLanguageServiceStateEvent:
|
||||
case ProjectLanguageServiceStateEvent: {
|
||||
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
|
||||
this.event<protocol.ProjectLanguageServiceStateEventBody>({
|
||||
projectName: event.data.project.getProjectName(),
|
||||
languageServiceEnabled: event.data.languageServiceEnabled
|
||||
}, eventName);
|
||||
break;
|
||||
}
|
||||
case ProjectInfoTelemetryEvent: {
|
||||
const eventName: protocol.TelemetryEventName = "telemetry";
|
||||
this.event<protocol.TelemetryEventBody>({
|
||||
telemetryEventName: event.eventName,
|
||||
payload: event.data,
|
||||
}, eventName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user