@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vscode/telemetry-extractor",
3
- "version": "1.9.5",
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.0",
26
- "ts-morph": "^12.2.0",
27
- "vscode-ripgrep": "^1.12.1"
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.0.0",
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.20",
36
- "ts-node": "^10.4.0",
37
- "typescript": "^4.4.4"
35
+ "source-map-support": "^0.5.21",
36
+ "ts-node": "^10.9.1",
37
+ "typescript": "^4.7.4"
38
38
  }
39
39
  }
@@ -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 { IProperty, IInclude, ITelemetryDataPoint, ITelemetryData, IInline, IWildcard, IWildcardEntry } from './telemetry-interfaces';
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
  }
@@ -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
  }
@@ -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-ripgrep';
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 {
@@ -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-ripgrep";
7
+ import { rgPath } from "@vscode/ripgrep";
8
8
  import { makeExclusionsRelativeToSource } from "./operations";
9
-
10
- interface IGDPRProperty {
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[currentNode.getEscapedName()] = type.getText().substring(1, type.getText().length - 1);
49
+ this.resolved_property[nodeName] = type.getText().substring(1, type.getText().length - 1);
66
50
  } else {
67
- this.resolved_property[currentNode.getEscapedName()] = type.getText() === 'true';
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
- public resolveProperties(currentNode: Symbol) {
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('Unable to resolve event name, skipping....');
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 GDPREvent(event_name);
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.