@vscode/telemetry-extractor 1.9.5 → 1.9.8
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.
- package/.github/workflows/node.js.yml +31 -0
- package/README.md +1 -1
- package/out/cli-options.js +6 -1
- package/out/cli-options.js.map +1 -1
- package/out/extractor.js +7 -3
- package/out/extractor.js.map +1 -1
- package/out/lib/events.js +15 -1
- package/out/lib/events.js.map +1 -1
- package/out/lib/file-writer.js +5 -1
- package/out/lib/file-writer.js.map +1 -1
- package/out/lib/fragments.js.map +1 -1
- package/out/lib/object-converter.js +15 -2
- package/out/lib/object-converter.js.map +1 -1
- package/out/lib/operations.js +9 -1
- package/out/lib/operations.js.map +1 -1
- package/out/lib/parser.js +12 -4
- package/out/lib/parser.js.map +1 -1
- package/out/lib/save-declarations.js +14 -1
- package/out/lib/save-declarations.js.map +1 -1
- package/out/lib/source-spec.js +5 -1
- package/out/lib/source-spec.js.map +1 -1
- package/out/lib/ts-parser.js +31 -15
- package/out/lib/ts-parser.js.map +1 -1
- package/package.json +9 -9
- package/src/cli-options.ts +1 -0
- package/src/extractor.ts +2 -2
- package/src/lib/events.ts +17 -2
- package/src/lib/fragments.ts +2 -2
- package/src/lib/object-converter.ts +10 -3
- package/src/lib/operations.ts +4 -1
- package/src/lib/parser.ts +7 -3
- package/src/lib/save-declarations.ts +9 -0
- package/src/lib/telemetry-interfaces.ts +6 -1
- package/src/lib/ts-parser.ts +28 -25
- package/documentation/comment-code-annotations.md +0 -197
- package/documentation/typescript-code-annotations.md +0 -187
- package/documentation/using-the-tool.md +0 -106
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vscode/telemetry-extractor",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.8",
|
|
4
4
|
"description": "Extracts telemetry from VS Code",
|
|
5
5
|
"main": "out/index.js",
|
|
6
6
|
"typings": "vscode-telemetry-extractor.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "mocha out/tests/mocha/*.js",
|
|
13
13
|
"coverage": "nyc mocha out/tests/mocha/*.js",
|
|
14
|
-
"extract-core": "node ./out/extractor.js --sourceDir src/telemetry-sources/vscode --excludedDir src/telemetry-sources/vscode/extensions --eventPrefix monacoworkbench/ --outputDir . --applyEndpoints",
|
|
14
|
+
"extract-core": "node ./out/extractor.js --sourceDir src/telemetry-sources/vscode --excludedDir src/telemetry-sources/vscode/extensions --eventPrefix monacoworkbench/ --outputDir . --applyEndpoints -f telemetry",
|
|
15
15
|
"extract-core-verbose": "node ./out/extractor.js --sourceDir src/telemetry-sources/vscode --excludedDir src/telemetry-sources/vscode/extensions --eventPrefix monacoworkbench/ --outputDir . --applyEndpoints --silent --verbose",
|
|
16
16
|
"compile": "tsc",
|
|
17
17
|
"watch": "tsc --watch"
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"author": "lramos15",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"command-line-args": "^5.2.
|
|
26
|
-
"ts-morph": "^
|
|
27
|
-
"vscode
|
|
25
|
+
"command-line-args": "^5.2.1",
|
|
26
|
+
"ts-morph": "^15.1.0",
|
|
27
|
+
"@vscode/ripgrep": "^1.14.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/command-line-args": "^5.2.0",
|
|
31
|
-
"@types/mocha": "^9.
|
|
31
|
+
"@types/mocha": "^9.1.1",
|
|
32
32
|
"@types/node": "^15.12.2",
|
|
33
33
|
"mocha": "^9.1.3",
|
|
34
34
|
"nyc": "^15.1.0",
|
|
35
|
-
"source-map-support": "^0.5.
|
|
36
|
-
"ts-node": "^10.
|
|
37
|
-
"typescript": "^4.
|
|
35
|
+
"source-map-support": "^0.5.21",
|
|
36
|
+
"ts-node": "^10.9.1",
|
|
37
|
+
"typescript": "^4.7.4"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/cli-options.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const optionDefinitions = [
|
|
|
21
21
|
{ name: 'excludedDir', alias: 'x', description: 'A subdirectory which you would like to exclude from the extraction (relative to the CWD)', type: String, multiple: true, defaultValue: [] },
|
|
22
22
|
{ name: 'config', alias: 'c', description: 'A JSON Configuration file containing extraction details', type: String },
|
|
23
23
|
{ name: 'outputDir', alias: 'o', description: 'The directory which you would like the outputted JSON file to be placed in', type: String },
|
|
24
|
+
{ name: 'fileName', alias: 'f', description: 'The name of the outputted JSON file', type: String },
|
|
24
25
|
{ name: 'eventPrefix', alias: 'p', type: String, description: 'The string you wish to prepend to every telemetry event.', defaultValue: '' },
|
|
25
26
|
{ name: 'help', alias: 'h', type: Boolean, description: 'Displays the help dialog which provides more information on how to use the tool', defaultValue: false },
|
|
26
27
|
{ name: 'applyEndpoints', alias: 'e', type: Boolean, defaultValue: false },
|
package/src/extractor.ts
CHANGED
|
@@ -12,7 +12,7 @@ if (options.config) {
|
|
|
12
12
|
const sourceSpecs = convertConfigToSourceSpecs(options.config);
|
|
13
13
|
extractAndResolveDeclarations(sourceSpecs).then((declarations) => {
|
|
14
14
|
if (options.outputDir) {
|
|
15
|
-
writeToFile(options.outputDir, declarations, 'config-resolved', !options.silent);
|
|
15
|
+
writeToFile(options.outputDir, declarations, options.fileName || 'config-resolved', !options.silent);
|
|
16
16
|
} else {
|
|
17
17
|
console.log(JSON.stringify(declarations));
|
|
18
18
|
}
|
|
@@ -36,7 +36,7 @@ if (options.config) {
|
|
|
36
36
|
};
|
|
37
37
|
extractAndResolveDeclarations([sourceSpec]).then((declarations) => {
|
|
38
38
|
if (options.outputDir) {
|
|
39
|
-
writeToFile(options.outputDir, declarations, 'declarations-resolved', !options.silent);
|
|
39
|
+
writeToFile(options.outputDir, declarations, options.fileName || 'declarations-resolved', !options.silent);
|
|
40
40
|
} else {
|
|
41
41
|
console.log(JSON.stringify(declarations));
|
|
42
42
|
}
|
package/src/lib/events.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
|
-
import {
|
|
3
|
+
import { IInclude, ITelemetryDataPoint, ITelemetryData, IInline, IWildcard, IWildcardEntry, IMetadata } from './telemetry-interfaces';
|
|
4
4
|
import { Property } from './common-properties';
|
|
5
5
|
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ export class Events implements ITelemetryData {
|
|
|
14
14
|
export class Event implements ITelemetryDataPoint {
|
|
15
15
|
public name: string;
|
|
16
16
|
// It gets a little more complicated here as events can have a bunch of different things
|
|
17
|
-
public properties: Array<Property | Include | Inline | Wildcard>;
|
|
17
|
+
public properties: Array<Property | Metadata | Include | Inline | Wildcard>;
|
|
18
18
|
constructor (name: string) {
|
|
19
19
|
this.name = name;
|
|
20
20
|
this.properties = [];
|
|
@@ -53,4 +53,19 @@ export class WildcardEntry implements IWildcardEntry {
|
|
|
53
53
|
this.classification = classification;
|
|
54
54
|
this.endpoint = endPoint;
|
|
55
55
|
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class Metadata implements IMetadata {
|
|
59
|
+
public name: 'owner' | 'comment' | 'expiration';
|
|
60
|
+
public value: string;
|
|
61
|
+
constructor (name: 'owner' | 'comment' | 'expiration', value: string) {
|
|
62
|
+
this.name = name;
|
|
63
|
+
this.value = value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
simpleObject(): {[key: string]: string} {
|
|
67
|
+
const simple = Object.create(null);
|
|
68
|
+
simple[this.name] = this.value;
|
|
69
|
+
return simple;
|
|
70
|
+
}
|
|
56
71
|
}
|
package/src/lib/fragments.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
3
|
import { ITelemetryData, ITelemetryDataPoint, IProperty, IWildcard } from './telemetry-interfaces';
|
|
4
4
|
import { Property } from './common-properties';
|
|
5
|
-
import { Include, Inline } from './events';
|
|
5
|
+
import { Include, Inline, Metadata } from './events';
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
// Fragments are retrieved as an object of objects of objects. So this just makes it easier to see the structure.
|
|
@@ -15,7 +15,7 @@ export class Fragments implements ITelemetryData{
|
|
|
15
15
|
|
|
16
16
|
export class Fragment implements ITelemetryDataPoint {
|
|
17
17
|
public name: string;
|
|
18
|
-
public properties: Array<Property | Include | Inline | IWildcard>;
|
|
18
|
+
public properties: Array<Property | Metadata | Include | Inline | IWildcard>;
|
|
19
19
|
constructor (name: string) {
|
|
20
20
|
this.name = name;
|
|
21
21
|
this.properties = [];
|
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
3
|
import { OutputtedDeclarations } from "./declarations";
|
|
4
4
|
import { Property } from "./common-properties";
|
|
5
|
-
import { Wildcard } from "./events";
|
|
5
|
+
import { Metadata, Wildcard } from "./events";
|
|
6
6
|
import * as keywords from './keywords';
|
|
7
7
|
|
|
8
8
|
// Converts the declarations array to an object format for easy readability.
|
|
9
9
|
|
|
10
10
|
export async function transformOutput(output: OutputtedDeclarations): Promise<OutputtedDeclarations> {
|
|
11
|
+
// If there's no events or common properties, we emit a null object
|
|
12
|
+
if (output.events.dataPoints.length === 0 && output.commonProperties.properties.length === 0) {
|
|
13
|
+
return Object.create(null);
|
|
14
|
+
}
|
|
11
15
|
const newEvents = Object.create(null);
|
|
12
16
|
const oldEvents = output.events.dataPoints;
|
|
13
17
|
for (const event of oldEvents) {
|
|
14
18
|
newEvents[event.name] = Object.create(null);
|
|
15
19
|
//We know there won't be anymore includes or inlines because we have resolved them
|
|
16
|
-
for (const property of event.properties as Array<Property | Wildcard>) {
|
|
20
|
+
for (const property of event.properties as Array<Property | Wildcard | Metadata>) {
|
|
17
21
|
if (property instanceof Wildcard) {
|
|
18
22
|
newEvents[event.name][keywords.wildcard] = newEvents[event.name][keywords.wildcard] ? newEvents[event.name][keywords.wildcard] : [];
|
|
19
23
|
for (const entry of property.entries) {
|
|
@@ -30,7 +34,7 @@ export async function transformOutput(output: OutputtedDeclarations): Promise<Ou
|
|
|
30
34
|
}
|
|
31
35
|
newEvents[event.name][keywords.wildcard].push(newEntry);
|
|
32
36
|
}
|
|
33
|
-
} else {
|
|
37
|
+
} else if (property instanceof Property) {
|
|
34
38
|
// Handles the case where the comments can be inconsistent
|
|
35
39
|
// We want to ensure that if isMeasurement is ever flagged it gets propogated
|
|
36
40
|
if (newEvents[event.name][propetyNameChanger(property.name)]) {
|
|
@@ -53,6 +57,9 @@ export async function transformOutput(output: OutputtedDeclarations): Promise<Ou
|
|
|
53
57
|
if (property.isMeasurement) {
|
|
54
58
|
newEvents[event.name][propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
|
|
55
59
|
}
|
|
60
|
+
} else {
|
|
61
|
+
// Comments, expiration, and owner metadata are handled here
|
|
62
|
+
newEvents[event.name][propetyNameChanger(property.name)] = property.value;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
}
|
package/src/lib/operations.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
3
|
import { Fragments, Fragment } from "./fragments";
|
|
4
|
-
import { Events, Event, Include, Inline, Wildcard, WildcardEntry } from "./events";
|
|
4
|
+
import { Events, Event, Include, Inline, Wildcard, WildcardEntry, Metadata } from "./events";
|
|
5
5
|
import { Property } from "./common-properties";
|
|
6
6
|
import * as keywords from './keywords';
|
|
7
7
|
|
|
@@ -64,6 +64,9 @@ export function populateProperties(properties: any, target: Event | Fragment, ap
|
|
|
64
64
|
target.properties.push(new Inline(propertyName, currentProperty[keywords.inline]));
|
|
65
65
|
} else if (propertyName === keywords.wildcard) {
|
|
66
66
|
mergeWildcards(currentProperty, target, applyEndpoints);
|
|
67
|
+
} else if (propertyName === 'owner' || propertyName === 'comment' || propertyName === 'expiration') {
|
|
68
|
+
// This is the special case when the property name matches one of our metadata properties
|
|
69
|
+
target.properties.push(new Metadata(propertyName, currentProperty));
|
|
67
70
|
} else {
|
|
68
71
|
const prop = new Property(propertyName, currentProperty.classification, currentProperty.purpose, currentProperty.expiration, currentProperty.owner, currentProperty.comment);
|
|
69
72
|
if (applyEndpoints) {
|
package/src/lib/parser.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT license.
|
|
3
|
-
import { rgPath } from 'vscode
|
|
3
|
+
import { rgPath } from '@vscode/ripgrep';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as cp from 'child_process';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
+
// Not importing 'process' as tsc claims `process.exitCode` is read-only when it actually is not.
|
|
7
8
|
import { Fragments } from './fragments';
|
|
8
9
|
import { Property, CommonProperties } from './common-properties';
|
|
9
10
|
import { Events } from './events';
|
|
@@ -79,6 +80,7 @@ export class Parser {
|
|
|
79
80
|
} catch (error) {
|
|
80
81
|
console.error(`Common Property Declaration Error: ${error} in file ${filePath}`);
|
|
81
82
|
console.error(`Source comment:\n${match[0]}`);
|
|
83
|
+
process.exitCode = 1;
|
|
82
84
|
}
|
|
83
85
|
});
|
|
84
86
|
return commonPropertyDeclarations;
|
|
@@ -90,7 +92,7 @@ export class Parser {
|
|
|
90
92
|
return this.findFiles(ripgrepPattern, sourceDir);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
///
|
|
95
|
+
///
|
|
94
96
|
private findFragments(sourceDir: string) {
|
|
95
97
|
const filesWithFragments = this.asAbsoluteFilePaths(this.findFilesWithFragments(sourceDir));
|
|
96
98
|
|
|
@@ -109,6 +111,7 @@ export class Parser {
|
|
|
109
111
|
} catch (error) {
|
|
110
112
|
console.error(`Fragment Declaration Error: ${error} in file ${filePath}`);
|
|
111
113
|
console.error(`Source comment:\n${match[0]}`);
|
|
114
|
+
process.exitCode = 1;
|
|
112
115
|
}
|
|
113
116
|
});
|
|
114
117
|
return fragmentDeclarations;
|
|
@@ -138,6 +141,7 @@ export class Parser {
|
|
|
138
141
|
} catch (error) {
|
|
139
142
|
console.error(`Event Declaration Error: ${error} in file ${filePath}`);
|
|
140
143
|
console.error(`Source comment:\n${match[0]}`);
|
|
144
|
+
process.exitCode = 1;
|
|
141
145
|
}
|
|
142
146
|
});
|
|
143
147
|
return eventDeclarations;
|
|
@@ -181,4 +185,4 @@ export class Parser {
|
|
|
181
185
|
});
|
|
182
186
|
});
|
|
183
187
|
}
|
|
184
|
-
}
|
|
188
|
+
}
|
|
@@ -13,6 +13,10 @@ import { ParserOptions, SourceSpec } from './source-spec';
|
|
|
13
13
|
import { logMessage } from './logger';
|
|
14
14
|
|
|
15
15
|
export function writeToFile(outputDir: string, contents: object, fileName: string, emitProgressMessage: boolean) {
|
|
16
|
+
if (Object.keys(contents).length === 0) {
|
|
17
|
+
logMessage(`...no events found, skipping file emmision!`, !emitProgressMessage);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
const json = JSON.stringify(contents);
|
|
17
21
|
const outputFile = path.resolve(outputDir, `${fileName}.json`);
|
|
18
22
|
logMessage(`...writing ${outputFile}`, !emitProgressMessage);
|
|
@@ -61,6 +65,11 @@ export async function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpe
|
|
|
61
65
|
}
|
|
62
66
|
const formattedDeclarations: any = await transformOutput(allDeclarations);
|
|
63
67
|
for (const dec in allTypeScriptDeclarations) {
|
|
68
|
+
// If there's typescript declarations but we returned a null object
|
|
69
|
+
// We must add the event container to the declarations object
|
|
70
|
+
if (formattedDeclarations.events === undefined) {
|
|
71
|
+
formattedDeclarations.events = Object.create(null);
|
|
72
|
+
}
|
|
64
73
|
formattedDeclarations.events[dec] = allTypeScriptDeclarations[dec];
|
|
65
74
|
}
|
|
66
75
|
return Promise.resolve(formattedDeclarations);
|
|
@@ -8,7 +8,12 @@ export interface ITelemetryData{
|
|
|
8
8
|
|
|
9
9
|
export interface ITelemetryDataPoint {
|
|
10
10
|
name: string;
|
|
11
|
-
properties: Array<IProperty | IInclude | IInline | Wildcard>;
|
|
11
|
+
properties: Array<IProperty | IInclude | IInline | IMetadata | Wildcard>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IMetadata {
|
|
15
|
+
name: 'owner' | 'comment' | 'expiration';
|
|
16
|
+
value: string;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export interface IProperty {
|
package/src/lib/ts-parser.ts
CHANGED
|
@@ -4,27 +4,10 @@ import { Project, SyntaxKind, Symbol, Node, CallExpression } 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 { rgPath } from "vscode
|
|
7
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
8
8
|
import { makeExclusionsRelativeToSource } from "./operations";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
propName: string;
|
|
12
|
-
classification: 'SystemMetaData' | 'CallstackOrException';
|
|
13
|
-
purpose: 'PerformanceAndHealth' | 'FeatureInsight';
|
|
14
|
-
expiration?: string;
|
|
15
|
-
owner?: string;
|
|
16
|
-
comment?: string;
|
|
17
|
-
endpoint?: string;
|
|
18
|
-
isMeasurement?: boolean;
|
|
19
|
-
}
|
|
20
|
-
class GDPREvent {
|
|
21
|
-
public eventName: string;
|
|
22
|
-
public properties: Array<IGDPRProperty>;
|
|
23
|
-
constructor(name: string) {
|
|
24
|
-
this.eventName = name;
|
|
25
|
-
this.properties = [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
9
|
+
import { Event, Metadata } from './events';
|
|
10
|
+
import { Property } from "./common-properties";
|
|
28
11
|
|
|
29
12
|
class NodeVisitor {
|
|
30
13
|
|
|
@@ -60,11 +43,12 @@ class NodeVisitor {
|
|
|
60
43
|
// If we don't want endpoints skip them
|
|
61
44
|
if (currentNode.getEscapedName().toLowerCase() === "endpoint" && !this.applyEndpoints) return;
|
|
62
45
|
|
|
46
|
+
const nodeName = currentNode.getEscapedName();
|
|
63
47
|
// If it's a string we strip the quotes
|
|
64
48
|
if (type.isStringLiteral()) {
|
|
65
|
-
this.resolved_property[
|
|
49
|
+
this.resolved_property[nodeName] = type.getText().substring(1, type.getText().length - 1);
|
|
66
50
|
} else {
|
|
67
|
-
this.resolved_property[
|
|
51
|
+
this.resolved_property[nodeName] = type.getText() === 'true';
|
|
68
52
|
}
|
|
69
53
|
return;
|
|
70
54
|
}
|
|
@@ -87,7 +71,26 @@ class NodeVisitor {
|
|
|
87
71
|
this.prop_name = this.original_prop_name;
|
|
88
72
|
}
|
|
89
73
|
|
|
90
|
-
|
|
74
|
+
private visitMetadataNode(currentNode: Symbol) {
|
|
75
|
+
let type = currentNode.getTypeAtLocation(this.pl_node);
|
|
76
|
+
// If we mark a property as optional then it is nullable, however we want all properties
|
|
77
|
+
// So we want its non nullable type tl;dr this chops off the | undefined
|
|
78
|
+
if (type.isNullable()) {
|
|
79
|
+
type = type.getNonNullableType();
|
|
80
|
+
}
|
|
81
|
+
if (type.isStringLiteral()) {
|
|
82
|
+
const nodeName = currentNode.getEscapedName();
|
|
83
|
+
this.resolved_property[nodeName] = type.getText().substring(1, type.getText().length - 1);
|
|
84
|
+
if (nodeName === 'owner' || nodeName === 'comment' || nodeName === 'expiration') {
|
|
85
|
+
this.properties.push(new Metadata(nodeName, this.resolved_property[nodeName]).simpleObject());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public resolveProperties(currentNode: Symbol): Array<Property | Metadata> {
|
|
91
|
+
// It could be a complex node with nested types or a simple node with a string literal
|
|
92
|
+
// representing some kind of metadata, so we try both visitors.
|
|
93
|
+
this.visitMetadataNode(currentNode);
|
|
91
94
|
this.visitNode(currentNode);
|
|
92
95
|
return this.properties;
|
|
93
96
|
}
|
|
@@ -163,13 +166,13 @@ export class TsParser {
|
|
|
163
166
|
let event_name = pl.getArguments()[0].getType().isStringLiteral() ? pl.getArguments()[0].getType().getText() : '';
|
|
164
167
|
// If we can't resolve the event_name there is no use continuing
|
|
165
168
|
if (event_name === '') {
|
|
166
|
-
console.error(
|
|
169
|
+
console.error(`Unable to resolve event name ${pl.getFullText().trim()}, skipping....`);
|
|
167
170
|
return;
|
|
168
171
|
} else {
|
|
169
172
|
event_name = event_name.substring(1, event_name.length - 1);
|
|
170
173
|
}
|
|
171
174
|
event_name = this.lowerCaseEvents ? event_name.toLowerCase() : event_name;
|
|
172
|
-
const created_event = new
|
|
175
|
+
const created_event = new Event(event_name);
|
|
173
176
|
// We want the second one because public log is in the form <Event, Classification> and we care about the classification
|
|
174
177
|
const type_properties = typeArgs[1].getType().getProperties();
|
|
175
178
|
type_properties.forEach((prop) => {
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# Annotating your Code
|
|
2
|
-
|
|
3
|
-
**NOTE**: If using Typescript we recommend annotating your telemetry events with typings as shown [here](typescript-code-annotations.md)
|
|
4
|
-
|
|
5
|
-
All telemetry events we send need to be described. For each property of each event we need to know what kind of data they contain and for what reason we collect the data.
|
|
6
|
-
|
|
7
|
-
Let's assume we send the following event and the timer data is dynamic, i.e. the properties of `timer` can not be known statically:
|
|
8
|
-
```ts
|
|
9
|
-
logEvent('E1', {
|
|
10
|
-
E1P1: 'activitybar',
|
|
11
|
-
...f1,
|
|
12
|
-
...f4,
|
|
13
|
-
timer: {
|
|
14
|
-
waited: 536,
|
|
15
|
-
processing: 43,
|
|
16
|
-
queued: 97,
|
|
17
|
-
elasped: 812
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
The event is constructed in multiple steps, normally scattered across several files:
|
|
23
|
-
```ts
|
|
24
|
-
function logEvent(eventName, eventData) {
|
|
25
|
-
eventData.CP1 = getSQMUserId();
|
|
26
|
-
service.sendEvent(eventName, eventData);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let f1 : F1 = { F1P1 : 23 };
|
|
30
|
-
|
|
31
|
-
let f2 : F2 = { F2P1 : document.getLine(1) };
|
|
32
|
-
|
|
33
|
-
let f3 : F3 = { F3P1: publisher.displayName };
|
|
34
|
-
|
|
35
|
-
let f4 : F4 = {
|
|
36
|
-
F4P1: extension.extensionName,
|
|
37
|
-
F4P2: {
|
|
38
|
-
...f2,
|
|
39
|
-
...f3
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
logEvent('E1', {
|
|
44
|
-
E1P1: 'activitybar',
|
|
45
|
-
...f1,
|
|
46
|
-
...f4,
|
|
47
|
-
timer: {
|
|
48
|
-
waited: 536,
|
|
49
|
-
processing: 43,
|
|
50
|
-
queued: 97,
|
|
51
|
-
elasped: 812
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
In order to extract the event descriptions with simple scanners we use specific comments to describe telemetry events and their properties. We place those comments as close as possible to where the events and properties are generated. The code above would be annotated as follows:
|
|
57
|
-
```ts
|
|
58
|
-
// __GDPR__COMMON__ "CP1" : { "endPoint": "SqmUserId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" }
|
|
59
|
-
function logEvent(eventName, eventData) {
|
|
60
|
-
eventData.CP1 = getSQMUserId();
|
|
61
|
-
service.sendEvent(eventName, eventData);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* __GDPR__FRAGMENT__
|
|
65
|
-
"F1" : {
|
|
66
|
-
"F1P1": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
67
|
-
}
|
|
68
|
-
*/
|
|
69
|
-
let f1 : F1 = { F1P1 : 23 };
|
|
70
|
-
|
|
71
|
-
/* __GDPR__FRAGMENT__
|
|
72
|
-
"F2" : {
|
|
73
|
-
"F2P1" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" }
|
|
74
|
-
}
|
|
75
|
-
*/
|
|
76
|
-
let f2 : F2 = { F2P1 : document.getLine(1) };
|
|
77
|
-
|
|
78
|
-
/* __GDPR__FRAGMENT__
|
|
79
|
-
"F3" : {
|
|
80
|
-
"F3P1" : { "classification": "PublicPersonalData", "purpose": "FeatureInsight" }
|
|
81
|
-
}
|
|
82
|
-
*/
|
|
83
|
-
let f3 : F3 = { F3P1: publisher.displayName };
|
|
84
|
-
|
|
85
|
-
/* __GDPR__FRAGMENT__
|
|
86
|
-
"F4" : {
|
|
87
|
-
"F4P1" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
|
|
88
|
-
"F4P2": {
|
|
89
|
-
"${inline}": [
|
|
90
|
-
"${F2}",
|
|
91
|
-
"${F3}"
|
|
92
|
-
]
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
*/
|
|
96
|
-
let f4 : F4 = {
|
|
97
|
-
F4P1: extension.extensionName,
|
|
98
|
-
F4P2: {
|
|
99
|
-
...f2,
|
|
100
|
-
...f3
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/* __GDPR__
|
|
105
|
-
"E1" : {
|
|
106
|
-
"E1P1" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
|
107
|
-
"${include}": [
|
|
108
|
-
"${F1}",
|
|
109
|
-
"${F4}"
|
|
110
|
-
],
|
|
111
|
-
"${wildcard}": [
|
|
112
|
-
{
|
|
113
|
-
"${prefix}": "timer.",
|
|
114
|
-
"${classification}": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
*/
|
|
119
|
-
logEvent('E1', {
|
|
120
|
-
E1P1: 'activitybar',
|
|
121
|
-
...f1,
|
|
122
|
-
...f4,
|
|
123
|
-
timer: {
|
|
124
|
-
waited: 536,
|
|
125
|
-
processing: 43,
|
|
126
|
-
queued: 97,
|
|
127
|
-
elasped: 812
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The GDPR comments are processed and result in the following final description of the `E1` event. Every property that starts with `timer.` -- such as `timer.waited` -- is classified as system metadata that we collect for gaining insights into how the feature is being used.
|
|
133
|
-
```json
|
|
134
|
-
"E1" : {
|
|
135
|
-
"E1P1" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
|
136
|
-
"F1P1": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
|
137
|
-
"F4P1" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" },
|
|
138
|
-
"F4P2.F2P1" : { "classification": "CustomerContent", "purpose": "PerformanceAndHealth" },
|
|
139
|
-
"F4P2.F3P1" : { "classification": "PublicPersonalData", "purpose": "FeatureInsight" },
|
|
140
|
-
"${wildcard}": [
|
|
141
|
-
{
|
|
142
|
-
"${prefix}": "timer.",
|
|
143
|
-
"${classification}": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
144
|
-
}
|
|
145
|
-
],
|
|
146
|
-
"CP1" : { "endPoint": "SqmUserId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" }
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
# Syntax
|
|
151
|
-
All GDPR comments are tagged with one of the following tags and are otherwise well-formed `JSON`.
|
|
152
|
-
- `__GDPR__` - describes the name and the properties of a telemetry event
|
|
153
|
-
- `__GDPR__FRAGMENT__` - describes the name and the properties of a fragment of the data of an event, fragments are either included or inlined by other fragments or events
|
|
154
|
-
- `__GDPR__COMMON__` - describes a property added to every telemetry event
|
|
155
|
-
|
|
156
|
-
Each property is described with an object that looks like this:
|
|
157
|
-
```ts
|
|
158
|
-
{
|
|
159
|
-
endPoint?: "none" | "SqmUserId" | "SqmMachineId",
|
|
160
|
-
classification: "SystemMetaData" | "CustomerContent" | "EndUserPseudonymizedInformation" | "PublicPersonalData" | "PublicNonPersonalData" | "CallstackOrException",
|
|
161
|
-
purpose: "FeatureInsight" | "PerformanceAndHealth" | "BusinessInsight" | "SecurityAndAuditing",
|
|
162
|
-
owner: string,
|
|
163
|
-
comment: string,
|
|
164
|
-
expiration?: string,
|
|
165
|
-
isMeasurement?: Boolean
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
If `endPoint` is omitted, it defaults to `none`. That's appropriate for pretty much all properties rather than a couple of common properties.
|
|
170
|
-
|
|
171
|
-
The values for `classification` are mostly self-explaining. `EndUserPseudonymizedInformation` is what allows us to identify a particular user across time, although we don't know the actual identity of the user. `machineId` or `instanceId` fall in this category. `PublicPersonalData` and `PublicNonPersonalData` is information that users provide us with, for example, publisher information on the marketplace. `CustomerContent` is information the user generated such as urls of repositories or custom snippets. `CallstackOrException` is for error data like callbacks and exceptions. Everything else is `SystemMetaData`.
|
|
172
|
-
|
|
173
|
-
`purpose` is usually `FeatureInsight` or `PerformanceAndHealth`. We only use `BusinessInsight` for events generated by surveys.
|
|
174
|
-
|
|
175
|
-
`owner` is used to specify who is responsbile for the telemetry event.
|
|
176
|
-
|
|
177
|
-
`comment` is used to specify a reason for collecting the event. This is meant to be more descriptive than `classification` and `purpose`.
|
|
178
|
-
|
|
179
|
-
`expiration` is used if you would like to dictate the max product version this telemetry event should be sent in. This allows external tools to specify which events should be removed from the codebase.
|
|
180
|
-
|
|
181
|
-
`isMeasurement` is used if the property is a number. Numbers are handled differently in the telemetry system.
|
|
182
|
-
|
|
183
|
-
For an event with no properties, define an empty event:
|
|
184
|
-
```json
|
|
185
|
-
"event" : { }
|
|
186
|
-
```
|
|
187
|
-
This will be classified as `SystemMetadata` with a purpose of `FeatureInsight`.
|
|
188
|
-
|
|
189
|
-
## Special constructs
|
|
190
|
-
#### ${include}
|
|
191
|
-
If A includes B, this is equivalent to the union of A and B. Fragments are referenced using `${FragmentName}`.
|
|
192
|
-
|
|
193
|
-
#### ${inline}
|
|
194
|
-
If A inlines B at property P, all properties of B are added to A under the key `"P.<Name in B>"`. Fragments are referenced using `${FragmentName}`.
|
|
195
|
-
|
|
196
|
-
#### ${wildcard}
|
|
197
|
-
Wildcards can be used as a **temporary** workaround to describe dynamic properties that have a common prefix. In the long run all dynamic properties need to be removed and be sent as values of a static property. A wild card is an array of wildcard entries. Each entry has a `${prefix}` and a `${classification}` property. The value of `${prefix}` is a string representing the common prefix of all properties it matches. The value of `${classification}` is a property description detailed above.
|