From 7fb7eecf2cac44b8d57e03606b0f5e331807d9ba Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 9 May 2018 07:51:46 -0700 Subject: [PATCH] Add telemetry for open JS files (#23833) * Add telemetry for open JS files * Send event every time * Keep stats even for closed files * Remove tsCheckCountForOpenFilesTelemetry * Use 'info.path' * Update API --- src/harness/unittests/telemetry.ts | 39 ++++++++++++++++++- .../unittests/tsserverProjectSystem.ts | 19 ++++++--- src/server/editorServices.ts | 34 +++++++++++++++- src/server/project.ts | 5 ++- src/server/typingsCache.ts | 2 +- src/services/preProcess.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 22 ++++++++++- 7 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 2e3e299ac3b..e3649ae1c79 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -42,7 +42,7 @@ namespace ts.projectSystem { const et = new TestServerEventManager([...files, notIncludedFile, tsconfig]); et.service.openClientFile(files[0].path); et.assertProjectInfoTelemetryEvent({ - fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, + fileStats: fileStats({ ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }), compilerOptions, include: true, }); @@ -234,9 +234,44 @@ namespace ts.projectSystem { languageServiceEnabled: false, }); }); + + describe("open files telemetry", () => { + it("sends event for inferred project", () => { + const ajs = makeFile("/a.js", "// @ts-check\nconst x = 0;"); + const bjs = makeFile("/b.js"); + const et = new TestServerEventManager([ajs, bjs]); + + et.service.openClientFile(ajs.path); + et.assertOpenFileTelemetryEvent({ checkJs: true }); + + et.service.openClientFile(bjs.path); + et.assertOpenFileTelemetryEvent({ checkJs: false }); + + // No repeated send for opening a file seen before. + et.service.openClientFile(bjs.path); + et.assertNoOpenFilesTelemetryEvent(); + }); + + it("not for '.ts' file", () => { + const ats = makeFile("/a.ts", ""); + const et = new TestServerEventManager([ats]); + + et.service.openClientFile(ats.path); + et.assertNoOpenFilesTelemetryEvent(); + }); + + it("even for project with 'ts-check' in config", () => { + const file = makeFile("/a.js"); + const compilerOptions: CompilerOptions = { checkJs: true }; + const jsconfig = makeFile("/jsconfig.json", { compilerOptions }); + const et = new TestServerEventManager([jsconfig, file]); + et.service.openClientFile(file.path); + et.assertOpenFileTelemetryEvent({ checkJs: false }); + }); + }); }); function makeFile(path: string, content: {} = ""): File { - return { path, content: isString(content) ? "" : JSON.stringify(content) }; + return { path, content: isString(content) ? content : JSON.stringify(content) }; } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f54bd56f1ba..f26170ef6ae 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -215,7 +215,7 @@ namespace ts.projectSystem { } assertProjectInfoTelemetryEvent(partial: Partial, configFile?: string): void { - assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { + assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { projectId: Harness.mockHash(configFile || "/tsconfig.json"), fileStats: fileStats({ ts: 1 }), compilerOptions: {}, @@ -236,6 +236,13 @@ namespace ts.projectSystem { ...partial, }); } + + assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { + assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); + } + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); + } } class TestSession extends server.Session { @@ -2755,7 +2762,7 @@ namespace ts.projectSystem { const session = createSession(host, { canUseEvents: true, eventHandler: e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent || e.eventName === server.OpenFileInfoTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); @@ -2807,7 +2814,7 @@ namespace ts.projectSystem { const session = createSession(host, { canUseEvents: true, eventHandler: e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent || e.eventName === server.OpenFileInfoTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); @@ -6241,14 +6248,14 @@ namespace ts.projectSystem { function verifyNoCall(callback: CalledMaps) { const calledMap = calledMaps[callback]; - assert.equal(calledMap.size, 0, `${callback} shouldnt be called: ${arrayFrom(calledMap.keys())}`); + assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); } function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); } - function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: string[], nTimes: number) { + function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: ReadonlyArray, nTimes: number) { TestFSWithWatch.checkMultiMapEachKeyWithCount(callback, calledMaps[callback], expectedKeys, nTimes); } @@ -6256,7 +6263,7 @@ namespace ts.projectSystem { iterateOnCalledMaps(key => verifyNoCall(key)); } - function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: string[]) { + function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: ReadonlyArray) { verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); verifyNoCall(CalledMapsWithSingleArg.directoryExists); verifyNoCall(CalledMapsWithSingleArg.getDirectories); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2bc61c67210..9de36f0687f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -6,6 +6,7 @@ namespace ts.server { export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; export const ProjectInfoTelemetryEvent = "projectInfo"; + export const OpenFileInfoTelemetryEvent = "openFileInfo"; // tslint:enable variable-name export interface ProjectsUpdatedInBackgroundEvent { @@ -55,6 +56,20 @@ namespace ts.server { readonly version: string; } + /** + * Info that we may send about a file that was just opened. + * Info about a file will only be sent once per session, even if the file changes in ways that might affect the info. + * Currently this is only sent for '.js' files. + */ + export interface OpenFileInfoTelemetryEvent { + readonly eventName: typeof OpenFileInfoTelemetryEvent; + readonly data: OpenFileInfoTelemetryEventData; + } + + export interface OpenFileInfoTelemetryEventData { + readonly info: OpenFileInfo; + } + export interface ProjectInfoTypeAcquisitionData { readonly enable: boolean; // Actual values of include/exclude entries are scrubbed. @@ -70,7 +85,11 @@ namespace ts.server { readonly dts: number; } - export type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; + export interface OpenFileInfo { + readonly checkJs: boolean; + } + + export type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; @@ -325,6 +344,9 @@ namespace ts.server { * Container of all known scripts */ private readonly filenameToScriptInfo = createMap(); + // Set of all '.js' files ever opened. + private readonly allJsFilesForOpenFileTelemetry = createMap(); + /** * Map to the real path of the infos */ @@ -2095,9 +2117,19 @@ namespace ts.server { this.printProjects(); + this.telemetryOnOpenFile(info); return { configFileName, configFileErrors }; } + private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { + if (!this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) { + return; + } + + const info: OpenFileInfo = { checkJs: !!scriptInfo.getDefaultProject().getSourceFile(scriptInfo.path).checkJsDirective }; + this.eventHandler({ eventName: OpenFileInfoTelemetryEvent, data: { info } }); + } + /** * Close file whose contents is managed by the client * @param filename is absolute pathname diff --git a/src/server/project.ts b/src/server/project.ts index 328d41649ca..a19739d2ba1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -6,9 +6,12 @@ namespace ts.server { External } + /* @internal */ + export type Mutable = { -readonly [K in keyof T]: T[K]; }; + /* @internal */ export function countEachFileTypes(infos: ScriptInfo[]): FileStats { - const result = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 }; + const result: Mutable = { js: 0, jsx: 0, ts: 0, tsx: 0, dts: 0 }; for (const info of infos) { switch (info.scriptKind) { case ScriptKind.JS: diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index c255757481f..7440735ecb2 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -11,7 +11,7 @@ namespace ts.server { enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; - readonly globalTypingsCacheLocation: string; + readonly globalTypingsCacheLocation: string | undefined; } export const nullTypingsInstaller: ITypingsInstaller = { diff --git a/src/services/preProcess.ts b/src/services/preProcess.ts index 6c5f6c1b2ff..5f1378ddbb7 100644 --- a/src/services/preProcess.ts +++ b/src/services/preProcess.ts @@ -1,7 +1,7 @@ namespace ts { export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { const pragmaContext: PragmaContext = { - languageVersion: ScriptTarget.ES5, // controls weather the token scanner considers unicode identifiers or not - shouldn't matter, since we're only using it for trivia + languageVersion: ScriptTarget.ES5, // controls whether the token scanner considers unicode identifiers or not - shouldn't matter, since we're only using it for trivia pragmas: undefined, checkJsDirective: undefined, referencedFiles: [], diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index f6bc5e75781..845ce42b5dc 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7734,7 +7734,7 @@ declare namespace ts.server { enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; - readonly globalTypingsCacheLocation: string; + readonly globalTypingsCacheLocation: string | undefined; } const nullTypingsInstaller: ITypingsInstaller; } @@ -7971,6 +7971,7 @@ declare namespace ts.server { const ConfigFileDiagEvent = "configFileDiag"; const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; const ProjectInfoTelemetryEvent = "projectInfo"; + const OpenFileInfoTelemetryEvent = "openFileInfo"; interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { @@ -8019,6 +8020,18 @@ declare namespace ts.server { /** TypeScript version used by the server. */ readonly version: string; } + /** + * Info that we may send about a file that was just opened. + * Info about a file will only be sent once per session, even if the file changes in ways that might affect the info. + * Currently this is only sent for '.js' files. + */ + interface OpenFileInfoTelemetryEvent { + readonly eventName: typeof OpenFileInfoTelemetryEvent; + readonly data: OpenFileInfoTelemetryEventData; + } + interface OpenFileInfoTelemetryEventData { + readonly info: OpenFileInfo; + } interface ProjectInfoTypeAcquisitionData { readonly enable: boolean; readonly include: boolean; @@ -8031,7 +8044,10 @@ declare namespace ts.server { readonly tsx: number; readonly dts: number; } - type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; + interface OpenFileInfo { + readonly checkJs: boolean; + } + type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; interface SafeList { [name: string]: { @@ -8082,6 +8098,7 @@ declare namespace ts.server { * Container of all known scripts */ private readonly filenameToScriptInfo; + private readonly allJsFilesForOpenFileTelemetry; /** * maps external project file name to list of config files that were the part of this project */ @@ -8286,6 +8303,7 @@ declare namespace ts.server { openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult; private findExternalProjectContainingOpenScriptInfo; openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult; + private telemetryOnOpenFile; /** * Close file whose contents is managed by the client * @param filename is absolute pathname