@vscode/telemetry-extractor 1.20.4 → 1.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/node.js.yml +2 -2
  3. package/eslint.config.js +6 -1
  4. package/out/cli-options.js +2 -1
  5. package/out/cli-options.js.map +1 -1
  6. package/out/extractor.js +7 -1
  7. package/out/extractor.js.map +1 -1
  8. package/out/lib/common-properties.js +51 -1
  9. package/out/lib/common-properties.js.map +1 -1
  10. package/out/lib/declarations.js.map +1 -1
  11. package/out/lib/event-definition.js +5 -0
  12. package/out/lib/event-definition.js.map +1 -0
  13. package/out/lib/events.js +28 -1
  14. package/out/lib/events.js.map +1 -1
  15. package/out/lib/object-converter.js +58 -4
  16. package/out/lib/object-converter.js.map +1 -1
  17. package/out/lib/operations.js +20 -1
  18. package/out/lib/operations.js.map +1 -1
  19. package/out/lib/parser.js +12 -16
  20. package/out/lib/parser.js.map +1 -1
  21. package/out/lib/ripgrep.js +14 -0
  22. package/out/lib/ripgrep.js.map +1 -0
  23. package/out/lib/save-declarations.js +11 -6
  24. package/out/lib/save-declarations.js.map +1 -1
  25. package/out/lib/ts-parser-worker.js +39 -0
  26. package/out/lib/ts-parser-worker.js.map +1 -0
  27. package/out/lib/ts-parser.js +198 -31
  28. package/out/lib/ts-parser.js.map +1 -1
  29. package/package.json +2 -2
  30. package/src/cli-options.ts +2 -1
  31. package/src/extractor.ts +8 -1
  32. package/src/lib/common-properties.ts +71 -5
  33. package/src/lib/declarations.ts +2 -1
  34. package/src/lib/event-definition.ts +7 -0
  35. package/src/lib/events.ts +33 -1
  36. package/src/lib/object-converter.ts +73 -4
  37. package/src/lib/operations.ts +20 -2
  38. package/src/lib/parser.ts +16 -30
  39. package/src/lib/ripgrep.ts +10 -0
  40. package/src/lib/save-declarations.ts +15 -9
  41. package/src/lib/ts-parser-worker.ts +38 -0
  42. package/src/lib/ts-parser.ts +242 -39
  43. package/tsconfig.json +1 -0
  44. package/vscode-telemetry-extractor.d.ts +21 -1
@@ -1,22 +1,79 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT license.
3
- import { Project, SyntaxKind, Symbol, Node, CallExpression, Type } from "ts-morph";
3
+ import { Project, SyntaxKind, Symbol, Node, CallExpression, Type, ts, CompilerOptions } from "ts-morph";
4
4
  import * as fs from 'fs';
5
5
  import * as cp from 'child_process';
6
6
  import * as path from 'path';
7
+ import { Worker } from 'worker_threads';
7
8
  import { rgPath } from "@vscode/ripgrep";
8
9
  import { makeExclusionsRelativeToSource } from "./operations";
9
10
  import { Event, Metadata } from './events';
10
11
  import { Property } from "./common-properties";
12
+ import { parseRipgrepFilePaths } from './ripgrep';
13
+ import { EventDefinition } from './event-definition';
11
14
 
12
- interface EventPropertySignature {
13
- classification: string;
14
- purpose: string;
15
+ const sourceFilesPerBatch = 64;
16
+ type ParsedEvents = Record<string, Record<string, unknown>>;
17
+
18
+ interface TelemetryCall {
19
+ start: number;
20
+ width: number;
15
21
  }
16
22
 
17
- interface EventDefinition {
18
- properties: Record<string, EventPropertySignature>;
19
- location: string;
23
+ interface TelemetryCalls {
24
+ filePath: string;
25
+ calls: TelemetryCall[];
26
+ }
27
+
28
+ export type ParserWorkerRequest = {
29
+ kind: 'prepare';
30
+ sourceFiles: string[];
31
+ compilerOptions: CompilerOptions;
32
+ } | {
33
+ kind: 'parse';
34
+ calls: TelemetryCalls[];
35
+ sharedSourceFiles: string[];
36
+ compilerOptions: CompilerOptions;
37
+ applyEndpoints: boolean;
38
+ lowerCaseEvents: boolean;
39
+ events: ParsedEvents;
40
+ definitions: [string, EventDefinition[]][];
41
+ };
42
+
43
+ export type ParserWorkerResult = {
44
+ kind: 'prepared';
45
+ calls: TelemetryCalls[];
46
+ sharedSourceFiles: string[];
47
+ } | {
48
+ kind: 'parsed';
49
+ events: ParsedEvents;
50
+ definitions: [string, EventDefinition[]][];
51
+ };
52
+
53
+ export function runParserWorker(request: ParserWorkerRequest): Promise<ParserWorkerResult> {
54
+ return new Promise((resolve, reject) => {
55
+ const worker = new Worker(path.join(__dirname, 'ts-parser-worker.js'), { workerData: request });
56
+ let result: ParserWorkerResult | undefined;
57
+ let error: Error | undefined;
58
+ worker.once('message', (message: ParserWorkerResult) => {
59
+ result = message;
60
+ });
61
+ worker.once('error', workerError => {
62
+ error = workerError;
63
+ });
64
+ // Do not start another compiler until this worker has released its heap.
65
+ worker.once('exit', code => {
66
+ if (error) {
67
+ reject(error);
68
+ } else if (code !== 0) {
69
+ reject(new Error(`Telemetry parser worker exited with code ${code}`));
70
+ } else if (!result) {
71
+ reject(new Error('Telemetry parser worker exited without a result'));
72
+ } else {
73
+ resolve(result);
74
+ }
75
+ });
76
+ });
20
77
  }
21
78
 
22
79
  function isMeasurement(type: Type) {
@@ -115,7 +172,7 @@ class NodeVisitor {
115
172
 
116
173
  private visitNode(currentNode: Symbol, previousNode?: Symbol) {
117
174
  let type = currentNode.getTypeAtLocation(this.pl_node);
118
- // If we mark a property as optional then it is nullable, however we want all properties
175
+ // If we mark a property as optional then it is nullable, however we want all properties
119
176
  // So we want its non nullable type tl;dr this chops off the | undefined
120
177
  if (type.isNullable()) {
121
178
  type = type.getNonNullableType();
@@ -140,10 +197,23 @@ class NodeVisitor {
140
197
  }
141
198
  return;
142
199
  }
143
- const properties = type.getProperties();
144
- properties.forEach((prop) => {
145
- this.visitNode(prop, currentNode);
146
- });
200
+ const nodeName = currentNode.getEscapedName();
201
+ if (nodeName !== 'column') {
202
+ const properties = type.getProperties();
203
+ properties.forEach((prop) => {
204
+ this.visitNode(prop, currentNode);
205
+ });
206
+ } else {
207
+ const properties = type.getProperties();
208
+ const value = Object.create(null);
209
+ properties.forEach((prop) => {
210
+ const propType = prop.getTypeAtLocation(this.pl_node);
211
+ if (propType.isStringLiteral()) {
212
+ value[prop.getEscapedName()] = propType.getText().substring(1, propType.getText().length - 1);
213
+ }
214
+ });
215
+ this.resolved_property['column'] = value;
216
+ }
147
217
  // 95% of the time there is only one property in this array but inlines allow
148
218
  // for the number of properties found to be unpredictable so we must return an array
149
219
  if (this.inline && this.prop_name === this.original_prop_name) {
@@ -161,7 +231,7 @@ class NodeVisitor {
161
231
 
162
232
  private visitMetadataNode(currentNode: Symbol) {
163
233
  let type = currentNode.getTypeAtLocation(this.pl_node);
164
- // If we mark a property as optional then it is nullable, however we want all properties
234
+ // If we mark a property as optional then it is nullable, however we want all properties
165
235
  // So we want its non nullable type tl;dr this chops off the | undefined
166
236
  if (type.isNullable()) {
167
237
  type = type.getNonNullableType();
@@ -176,6 +246,9 @@ class NodeVisitor {
176
246
  }
177
247
 
178
248
  public resolveProperties(currentNode: Symbol): Array<Property | Metadata> {
249
+ // @lramos15 Actually this.properties is of type any[] and the property data pushing into
250
+ // the array is not an instance of Property.
251
+
179
252
  // It could be a complex node with nested types or a simple node with a string literal
180
253
  // representing some kind of metadata, so we try both visitors.
181
254
  this.visitMetadataNode(currentNode);
@@ -190,6 +263,7 @@ export class TsParser {
190
263
  private applyEndpoints: boolean;
191
264
  private lowerCaseEvents: boolean;
192
265
  private project: Project;
266
+ private sourceFiles: string[] = [];
193
267
  private eventDefinitions: Map<string, EventDefinition[]>;
194
268
  constructor(sourceDir: string, excludedDirs: string[], applyEndpoints: boolean, lowerCaseEvents: boolean) {
195
269
  this.sourceDir = sourceDir;
@@ -226,18 +300,69 @@ export class TsParser {
226
300
 
227
301
  const ripgrepArgs = ['--files-with-matches', ...rgGlobs, '--no-ignore', 'publicLog2|publicLogError2', this.sourceDir]
228
302
  try {
229
- const retrieved_paths = cp.execFileSync(rgPath, ripgrepArgs, { encoding: 'ascii' });
230
- // Split the paths into an array
231
- retrieved_paths.split(/(?:\r\n|\r|\n)/g).filter(path => path && path.length > 0).map((f) => {
232
- this.project.addSourceFileAtPathIfExists(f);
233
- return f;
234
- });
303
+ const retrievedPaths = cp.execFileSync(rgPath, ripgrepArgs, { encoding: 'ascii' });
304
+ this.sourceFiles = parseRipgrepFilePaths(retrievedPaths);
235
305
  // Empty catch because this fails when there are no typescript annotations which causes weird error messages
236
306
  } catch {
237
307
  // No-op
238
308
  }
239
309
  }
240
310
 
311
+ public async parseFiles() {
312
+ if (this.sourceFiles.length <= sourceFilesPerBatch) {
313
+ for (const file of this.sourceFiles) {
314
+ this.project.addSourceFileAtPathIfExists(file);
315
+ }
316
+ const parser = new TsProjectParser(this.project, this.applyEndpoints, this.lowerCaseEvents, [...this.eventDefinitions]);
317
+ const events = parser.parseFiles();
318
+ this.eventDefinitions = parser.getEventDefinitions();
319
+ return events;
320
+ }
321
+
322
+ const compilerOptions = this.project.getCompilerOptions();
323
+ const prepared = await runParserWorker({ kind: 'prepare', sourceFiles: this.sourceFiles, compilerOptions });
324
+ if (prepared.kind !== 'prepared') {
325
+ throw new Error('Telemetry parser worker did not return call locations');
326
+ }
327
+
328
+ let events: ParsedEvents = Object.create(null);
329
+ for (let index = 0; index < prepared.calls.length; index += sourceFilesPerBatch) {
330
+ const parsed = await runParserWorker({
331
+ kind: 'parse',
332
+ calls: prepared.calls.slice(index, index + sourceFilesPerBatch),
333
+ sharedSourceFiles: prepared.sharedSourceFiles,
334
+ compilerOptions,
335
+ applyEndpoints: this.applyEndpoints,
336
+ lowerCaseEvents: this.lowerCaseEvents,
337
+ events,
338
+ definitions: [...this.eventDefinitions]
339
+ });
340
+ if (parsed.kind !== 'parsed') {
341
+ throw new Error('Telemetry parser worker did not return declarations');
342
+ }
343
+ events = Object.assign(Object.create(null), parsed.events);
344
+ this.eventDefinitions = new Map(parsed.definitions);
345
+ }
346
+ return events;
347
+ }
348
+
349
+ public getEventDefinitions() {
350
+ return new Map([...this.eventDefinitions].map(([event, entries]) => [event, [...entries]]));
351
+ }
352
+ }
353
+
354
+ export class TsProjectParser {
355
+ private eventDefinitions: Map<string, EventDefinition[]>;
356
+
357
+ constructor(
358
+ private readonly project: Project,
359
+ private readonly applyEndpoints: boolean,
360
+ private readonly lowerCaseEvents: boolean,
361
+ definitions: [string, EventDefinition[]][] = []
362
+ ) {
363
+ this.eventDefinitions = new Map(definitions);
364
+ }
365
+
241
366
  public getEventDefinitions() {
242
367
  const definitions = new Map<string, EventDefinition[]>();
243
368
  for (const [eventName, entries] of this.eventDefinitions.entries()) {
@@ -246,35 +371,91 @@ export class TsParser {
246
371
  return definitions;
247
372
  }
248
373
 
249
- private addEventDefinition(eventName: string, properties: Record<string, EventPropertySignature>, location: string) {
374
+ private addEventDefinition(eventName: string, properties: Record<string, unknown>, location: string) {
250
375
  const existing = this.eventDefinitions.get(eventName) ?? [];
251
376
  existing.push({ properties, location });
252
377
  this.eventDefinitions.set(eventName, existing);
253
378
  }
254
379
 
255
- private extractConflictProperties(eventProperties: Record<string, unknown>): Record<string, EventPropertySignature> {
256
- const result: Record<string, EventPropertySignature> = {};
257
- for (const [key, value] of Object.entries(eventProperties)) {
258
- if (value && typeof value === 'object' && !Array.isArray(value)) {
259
- const obj = value as Record<string, unknown>;
260
- if (typeof obj.classification === 'string' && typeof obj.purpose === 'string') {
261
- result[key] = { classification: obj.classification, purpose: obj.purpose };
262
- }
263
- }
264
- }
265
- return result;
380
+ private extractConflictProperties(eventProperties: Record<string, unknown>): Record<string, unknown> {
381
+ return { ...eventProperties };
266
382
  }
267
383
 
268
- public parseFiles() {
269
- let publicLogUse: Array<CallExpression> = [];
384
+ private collectCalls(): TelemetryCalls[] {
385
+ const publicLogCalls: TelemetryCalls[] = [];
386
+ const publicLogErrorCalls: TelemetryCalls[] = [];
270
387
  this.project.getSourceFiles().forEach((source) => {
271
- const descendants = source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => c.getExpression().getText().includes('publicLog2') && c.getArguments().length > 0);
272
- const descendants2 = source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => c.getExpression().getText().includes('publicLogError2') && c.getArguments().length > 0);
273
- publicLogUse = descendants.concat(publicLogUse, descendants2);
388
+ const calls: TelemetryCall[] = [];
389
+ const errorCalls: TelemetryCall[] = [];
390
+ const sourceFile = source.compilerNode;
391
+ const visit = (node: ts.Node): void => {
392
+ if (ts.isCallExpression(node) && node.arguments.length > 0) {
393
+ const expression = node.expression.getText(sourceFile);
394
+ const isPublicLog = expression.includes('publicLog2');
395
+ const isPublicLogError = expression.includes('publicLogError2');
396
+ if ((isPublicLog || isPublicLogError) && node.arguments[0].getText(sourceFile) !== 'eventName') {
397
+ const call = { start: node.getStart(sourceFile), width: node.getWidth(sourceFile) };
398
+ if (isPublicLog) {
399
+ calls.push(call);
400
+ }
401
+ if (isPublicLogError) {
402
+ errorCalls.push(call);
403
+ }
404
+ }
405
+ }
406
+ ts.forEachChild(node, visit);
407
+ };
408
+ ts.forEachChild(sourceFile, visit);
409
+ if (calls.length > 0) {
410
+ publicLogCalls.unshift({ filePath: source.getFilePath(), calls });
411
+ }
412
+ if (errorCalls.length > 0) {
413
+ publicLogErrorCalls.push({ filePath: source.getFilePath(), calls: errorCalls });
414
+ }
274
415
  });
416
+ return publicLogCalls.concat(publicLogErrorCalls);
417
+ }
418
+
419
+ private getSharedSourceFiles(): string[] {
420
+ const program = this.project.getProgram().compilerObject;
421
+ return program.getSourceFiles().filter(source =>
422
+ !ts.isExternalModule(source) || source.statements.some(statement =>
423
+ ts.isModuleDeclaration(statement) &&
424
+ (ts.isStringLiteral(statement.name) || (statement.flags & ts.NodeFlags.GlobalAugmentation) !== 0))
425
+ ).map(source => source.fileName);
426
+ }
275
427
 
428
+ public prepare(): ParserWorkerResult {
429
+ const calls = this.collectCalls();
430
+ return {
431
+ kind: 'prepared',
432
+ calls,
433
+ sharedSourceFiles: calls.length > 0 ? this.getSharedSourceFiles() : []
434
+ };
435
+ }
436
+
437
+ private parseCalls(groups: TelemetryCalls[], parseCall: (call: CallExpression) => void): void {
438
+ for (const group of groups) {
439
+ const source = this.project.getSourceFileOrThrow(group.filePath);
440
+ for (const call of group.calls) {
441
+ const node = source.getDescendantAtStartWithWidth(call.start, call.width);
442
+ if (!node || !Node.isCallExpression(node)) {
443
+ throw new Error(`Could not locate telemetry call in ${group.filePath} at ${call.start}`);
444
+ }
445
+ parseCall(node);
446
+ }
447
+ }
448
+ }
449
+
450
+ public parseFiles(calls = this.collectCalls(), previousEvents?: ParsedEvents) {
276
451
  const events = Object.create(null);
277
- publicLogUse.forEach((pl) => {
452
+ if (previousEvents) {
453
+ // Structured cloning does not preserve dictionary prototypes.
454
+ for (const [name, properties] of Object.entries(previousEvents)) {
455
+ events[name] = Object.assign(Object.create(null), properties);
456
+ }
457
+ }
458
+ const parseCall = (pl: CallExpression): void => {
278
459
  try {
279
460
  const typeArgs = pl.getTypeArguments();
280
461
  if (typeArgs.length != 2) {
@@ -303,7 +484,18 @@ export class TsParser {
303
484
  type_properties.forEach((prop) => {
304
485
  const propName = prop.getEscapedName().toLowerCase();
305
486
  const node_visitor = new NodeVisitor(pl, propName, this.applyEndpoints);
306
- created_event.properties = created_event.properties.concat(node_visitor.resolveProperties(prop));
487
+ const resolved_properties = node_visitor.resolveProperties(prop);
488
+ for (const rp of resolved_properties) {
489
+ if (!(rp instanceof Metadata)) {
490
+ // This cast is necessary since rp is not of type Property although
491
+ // the resolveProperties claims it to be.
492
+ const propInfo = (rp as unknown as { [key: string]: object })[propName];
493
+ if (propInfo !== undefined) {
494
+ this.captureOriginalPropNameForColumnInformation(prop.getEscapedName(), propInfo);
495
+ }
496
+ }
497
+ }
498
+ created_event.properties = created_event.properties.concat(resolved_properties);
307
499
  });
308
500
  created_event.properties.forEach((prop) => {
309
501
  Object.assign(events[event_name], prop);
@@ -344,7 +536,18 @@ export class TsParser {
344
536
  events[event_name] = {};
345
537
  this.addEventDefinition(event_name, this.extractConflictProperties(events[event_name]), `${pl.getSourceFile().getFilePath()}:${pl.getStartLineNumber()}`);
346
538
  }
347
- });
539
+ };
540
+
541
+ this.parseCalls(calls, parseCall);
348
542
  return events;
349
543
  }
544
+
545
+ private captureOriginalPropNameForColumnInformation(propName: string, property: { type?: string; column?: { name?: string; type: string } }) {
546
+ if (property.column && property.column.name === undefined) {
547
+ property.column.name = propName;
548
+ } else if (typeof property.type === 'string') {
549
+ property.column = { name: propName, type: property.type };
550
+ delete property.type;
551
+ }
552
+ }
350
553
  }
package/tsconfig.json CHANGED
@@ -62,6 +62,7 @@
62
62
  "exclude": [
63
63
  "./src/telemetry-sources",
64
64
  "./src/tests/mocha/resources",
65
+ "./src/tests/mocha/tableResources",
65
66
  "**/*.d.ts"
66
67
  ]
67
68
  }
@@ -48,13 +48,33 @@ export interface Property {
48
48
  purpose: string;
49
49
  endPoint?: string;
50
50
  isMeasurement?: boolean;
51
+ column?: { name?: string; type: string };
52
+ }
53
+
54
+ export type ColumnType =
55
+ 'bool' |
56
+ 'int' |
57
+ 'long' |
58
+ 'real' |
59
+ 'decimal' |
60
+ 'dynamic' |
61
+ 'guid' |
62
+ 'string' |
63
+ 'datetime' |
64
+ 'timespan';
65
+
66
+ export interface TableInfo {
67
+ name: string;
68
+ commonProperties: 'standard';
69
+ backfill: boolean | string;
70
+ columns: { name: string; type: ColumnType; bag: { store: 'Measures' | 'Properties'; name: string }}[];
51
71
  }
52
72
 
53
73
  /**
54
74
  * Extracts and resolves all typescript declarations from a series of different sources into a formatted object
55
75
  * @param sourceSpecs The various sources and their options which you would like to extract from
56
76
  */
57
- export declare function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpec>): Promise<{ events: Events, commonProperties: CommonProperties }>;
77
+ export declare function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpec>): Promise<{ events: Events, commonProperties: CommonProperties, tableInfos: TableInfo[] }>;
58
78
 
59
79
  /**
60
80
  * Parses a valid extractor config file into an array of sourceSpecs that can be passed into an extract function