@vscode/telemetry-extractor 1.20.2 → 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 +3 -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 +83 -66
  18. package/out/lib/operations.js.map +1 -1
  19. package/out/lib/parser.js +18 -23
  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 +32 -10
  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 +204 -37
  28. package/out/lib/ts-parser.js.map +1 -1
  29. package/package.json +3 -3
  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 +75 -72
  38. package/src/lib/parser.ts +22 -34
  39. package/src/lib/ripgrep.ts +10 -0
  40. package/src/lib/save-declarations.ts +37 -13
  41. package/src/lib/ts-parser-worker.ts +38 -0
  42. package/src/lib/ts-parser.ts +247 -41
  43. package/tsconfig.json +1 -0
  44. package/vscode-telemetry-extractor.d.ts +21 -1
@@ -1,17 +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 EventDefinition {
13
- signature: string;
14
- location: string;
15
+ const sourceFilesPerBatch = 64;
16
+ type ParsedEvents = Record<string, Record<string, unknown>>;
17
+
18
+ interface TelemetryCall {
19
+ start: number;
20
+ width: number;
21
+ }
22
+
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
+ });
15
77
  }
16
78
 
17
79
  function isMeasurement(type: Type) {
@@ -110,7 +172,7 @@ class NodeVisitor {
110
172
 
111
173
  private visitNode(currentNode: Symbol, previousNode?: Symbol) {
112
174
  let type = currentNode.getTypeAtLocation(this.pl_node);
113
- // 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
114
176
  // So we want its non nullable type tl;dr this chops off the | undefined
115
177
  if (type.isNullable()) {
116
178
  type = type.getNonNullableType();
@@ -135,10 +197,23 @@ class NodeVisitor {
135
197
  }
136
198
  return;
137
199
  }
138
- const properties = type.getProperties();
139
- properties.forEach((prop) => {
140
- this.visitNode(prop, currentNode);
141
- });
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
+ }
142
217
  // 95% of the time there is only one property in this array but inlines allow
143
218
  // for the number of properties found to be unpredictable so we must return an array
144
219
  if (this.inline && this.prop_name === this.original_prop_name) {
@@ -156,7 +231,7 @@ class NodeVisitor {
156
231
 
157
232
  private visitMetadataNode(currentNode: Symbol) {
158
233
  let type = currentNode.getTypeAtLocation(this.pl_node);
159
- // 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
160
235
  // So we want its non nullable type tl;dr this chops off the | undefined
161
236
  if (type.isNullable()) {
162
237
  type = type.getNonNullableType();
@@ -171,6 +246,9 @@ class NodeVisitor {
171
246
  }
172
247
 
173
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
+
174
252
  // It could be a complex node with nested types or a simple node with a string literal
175
253
  // representing some kind of metadata, so we try both visitors.
176
254
  this.visitMetadataNode(currentNode);
@@ -185,6 +263,7 @@ export class TsParser {
185
263
  private applyEndpoints: boolean;
186
264
  private lowerCaseEvents: boolean;
187
265
  private project: Project;
266
+ private sourceFiles: string[] = [];
188
267
  private eventDefinitions: Map<string, EventDefinition[]>;
189
268
  constructor(sourceDir: string, excludedDirs: string[], applyEndpoints: boolean, lowerCaseEvents: boolean) {
190
269
  this.sourceDir = sourceDir;
@@ -221,18 +300,69 @@ export class TsParser {
221
300
 
222
301
  const ripgrepArgs = ['--files-with-matches', ...rgGlobs, '--no-ignore', 'publicLog2|publicLogError2', this.sourceDir]
223
302
  try {
224
- const retrieved_paths = cp.execFileSync(rgPath, ripgrepArgs, { encoding: 'ascii' });
225
- // Split the paths into an array
226
- retrieved_paths.split(/(?:\r\n|\r|\n)/g).filter(path => path && path.length > 0).map((f) => {
227
- this.project.addSourceFileAtPathIfExists(f);
228
- return f;
229
- });
303
+ const retrievedPaths = cp.execFileSync(rgPath, ripgrepArgs, { encoding: 'ascii' });
304
+ this.sourceFiles = parseRipgrepFilePaths(retrievedPaths);
230
305
  // Empty catch because this fails when there are no typescript annotations which causes weird error messages
231
306
  } catch {
232
307
  // No-op
233
308
  }
234
309
  }
235
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
+
236
366
  public getEventDefinitions() {
237
367
  const definitions = new Map<string, EventDefinition[]>();
238
368
  for (const [eventName, entries] of this.eventDefinitions.entries()) {
@@ -241,37 +371,91 @@ export class TsParser {
241
371
  return definitions;
242
372
  }
243
373
 
244
- private addEventDefinition(eventName: string, signature: string, location: string) {
374
+ private addEventDefinition(eventName: string, properties: Record<string, unknown>, location: string) {
245
375
  const existing = this.eventDefinitions.get(eventName) ?? [];
246
- existing.push({ signature, location });
376
+ existing.push({ properties, location });
247
377
  this.eventDefinitions.set(eventName, existing);
248
378
  }
249
379
 
250
- private stableSerialize(value: unknown): string {
251
- if (Array.isArray(value)) {
252
- return `[${value.map((entry) => this.stableSerialize(entry)).join(',')}]`;
253
- }
254
-
255
- if (value && typeof value === 'object') {
256
- const entries = Object.entries(value as Record<string, unknown>)
257
- .sort(([left], [right]) => left.localeCompare(right))
258
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${this.stableSerialize(entryValue)}`);
259
- return `{${entries.join(',')}}`;
260
- }
261
-
262
- return JSON.stringify(value);
380
+ private extractConflictProperties(eventProperties: Record<string, unknown>): Record<string, unknown> {
381
+ return { ...eventProperties };
263
382
  }
264
383
 
265
- public parseFiles() {
266
- let publicLogUse: Array<CallExpression> = [];
384
+ private collectCalls(): TelemetryCalls[] {
385
+ const publicLogCalls: TelemetryCalls[] = [];
386
+ const publicLogErrorCalls: TelemetryCalls[] = [];
267
387
  this.project.getSourceFiles().forEach((source) => {
268
- const descendants = source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => c.getExpression().getText().includes('publicLog2') && c.getArguments().length > 0);
269
- const descendants2 = source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((c) => c.getExpression().getText().includes('publicLogError2') && c.getArguments().length > 0);
270
- 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
+ }
271
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
+ }
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
+ }
272
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) {
273
451
  const events = Object.create(null);
274
- 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 => {
275
459
  try {
276
460
  const typeArgs = pl.getTypeArguments();
277
461
  if (typeArgs.length != 2) {
@@ -300,12 +484,23 @@ export class TsParser {
300
484
  type_properties.forEach((prop) => {
301
485
  const propName = prop.getEscapedName().toLowerCase();
302
486
  const node_visitor = new NodeVisitor(pl, propName, this.applyEndpoints);
303
- 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);
304
499
  });
305
500
  created_event.properties.forEach((prop) => {
306
501
  Object.assign(events[event_name], prop);
307
502
  });
308
- this.addEventDefinition(event_name, this.stableSerialize(events[event_name]), `${pl.getSourceFile().getFilePath()}:${pl.getStartLineNumber()}`);
503
+ this.addEventDefinition(event_name, this.extractConflictProperties(events[event_name]), `${pl.getSourceFile().getFilePath()}:${pl.getStartLineNumber()}`);
309
504
  const eventProperties = typeArgs[0].getType().getProperties();
310
505
  // Find all eventProperties that have a number or boolean type
311
506
  eventProperties.forEach((prop) => {
@@ -339,9 +534,20 @@ export class TsParser {
339
534
  }
340
535
  event_name = this.lowerCaseEvents ? event_name.toLowerCase() : event_name;
341
536
  events[event_name] = {};
342
- this.addEventDefinition(event_name, this.stableSerialize(events[event_name]), `${pl.getSourceFile().getFilePath()}:${pl.getStartLineNumber()}`);
537
+ this.addEventDefinition(event_name, this.extractConflictProperties(events[event_name]), `${pl.getSourceFile().getFilePath()}:${pl.getStartLineNumber()}`);
343
538
  }
344
- });
539
+ };
540
+
541
+ this.parseCalls(calls, parseCall);
345
542
  return events;
346
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
+ }
347
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