Skip to content

Commit a190707

Browse files
authored
update telemetry for settings changes (#1973)
update telemetry for settings changes
1 parent 79871a6 commit a190707

File tree

4 files changed

+200
-99
lines changed

4 files changed

+200
-99
lines changed

Extension/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,11 @@
168168
"None",
169169
"Error",
170170
"Warning",
171-
"Information"
171+
"Information",
172+
"Debug"
172173
],
173174
"default": "Error",
174-
"description": "The verbosity of logging in the Output Panel. The order of levels from least verbose to most verbose is: None < Error < Warning < Information.",
175+
"description": "The verbosity of logging in the Output Panel. The order of levels from least verbose to most verbose is: None < Error < Warning < Information < Debug.",
175176
"scope": "resource"
176177
},
177178
"C_Cpp.autoAddFileAssociations": {

Extension/src/LanguageServer/client.ts

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DataBinding } from './dataBinding';
2222
import minimatch = require("minimatch");
2323
import * as logger from '../logger';
2424
import { updateLanguageConfigurations } from './extension';
25+
import { SettingsTracker, getTracker } from './settingsTracker';
2526

2627
let ui: UI;
2728

@@ -115,97 +116,8 @@ const DebugProtocolNotification: NotificationType<OutputNotificationBody, void>
115116
const DebugLogNotification: NotificationType<OutputNotificationBody, void> = new NotificationType<OutputNotificationBody, void>('cpptools/debugLog');
116117
const InactiveRegionNotification: NotificationType<InactiveRegionParams, void> = new NotificationType<InactiveRegionParams, void>('cpptools/inactiveRegions');
117118

118-
const maxSettingLengthForTelemetry: number = 50;
119-
let previousCppSettings: { [key: string]: any } = {};
120119
let failureMessageShown: boolean = false;
121120

122-
/**
123-
* track settings changes for telemetry
124-
*/
125-
function collectSettingsForTelemetry(filter: (key: string, val: string, settings: vscode.WorkspaceConfiguration) => boolean, resource: vscode.Uri): { [key: string]: string } {
126-
let settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", resource);
127-
let result: { [key: string]: string } = {};
128-
129-
for (let key in settings) {
130-
if (settings.inspect(key).defaultValue === undefined) {
131-
continue; // ignore methods and settings that don't exist
132-
}
133-
let val: any = settings.get(key);
134-
if (val instanceof Object) {
135-
val = JSON.stringify(val, null, 2);
136-
}
137-
138-
// Skip values that don't match the setting's enum.
139-
let curSetting: any = util.packageJson.contributes.configuration.properties["C_Cpp." + key];
140-
if (curSetting) {
141-
let curEnum: any[] = curSetting["enum"];
142-
if (curEnum && curEnum.indexOf(val) === -1) {
143-
continue;
144-
}
145-
}
146-
147-
if (filter(key, val, settings)) {
148-
previousCppSettings[key] = val;
149-
switch (key.toLowerCase()) {
150-
case "clang_format_path": {
151-
continue;
152-
}
153-
case "clang_format_style":
154-
case "clang_format_fallbackstyle": {
155-
let newKey: string = key + "2";
156-
if (val) {
157-
switch (String(val).toLowerCase()) {
158-
case "visual studio":
159-
case "llvm":
160-
case "google":
161-
case "chromium":
162-
case "mozilla":
163-
case "webkit":
164-
case "file":
165-
case "none": {
166-
result[newKey] = String(previousCppSettings[key]);
167-
break;
168-
}
169-
default: {
170-
result[newKey] = "...";
171-
break;
172-
}
173-
}
174-
} else {
175-
result[newKey] = "null";
176-
}
177-
key = newKey;
178-
break;
179-
}
180-
default: {
181-
if (key.startsWith("default.")) {
182-
continue; // Don't log c_cpp_properties.json defaults since they may contain PII.
183-
}
184-
result[key] = String(previousCppSettings[key]);
185-
break;
186-
}
187-
}
188-
if (result[key].length > maxSettingLengthForTelemetry) {
189-
result[key] = result[key].substr(0, maxSettingLengthForTelemetry) + "...";
190-
}
191-
}
192-
}
193-
194-
return result;
195-
}
196-
197-
function initializeSettingsCache(resource: vscode.Uri): void {
198-
collectSettingsForTelemetry(() => true, resource);
199-
}
200-
201-
function getNonDefaultSettings(resource: vscode.Uri): { [key: string]: string } {
202-
let filter: (key: string, val: string, settings: vscode.WorkspaceConfiguration) => boolean = (key: string, val: string, settings: vscode.WorkspaceConfiguration) => {
203-
return val !== settings.inspect(key).defaultValue;
204-
};
205-
initializeSettingsCache(resource);
206-
return collectSettingsForTelemetry(filter, resource);
207-
}
208-
209121
interface ClientModel {
210122
isTagParsing: DataBinding<boolean>;
211123
isUpdatingIntelliSense: DataBinding<boolean>;
@@ -265,6 +177,7 @@ class DefaultClient implements Client {
265177
private crashTimes: number[] = [];
266178
private isSupported: boolean = true;
267179
private inactiveRegionsDecorations = new Map<string, DecorationRangesPair>();
180+
private settingsTracker: SettingsTracker;
268181

269182
// The "model" that is displayed via the UI (status bar).
270183
private model: ClientModel = {
@@ -334,7 +247,8 @@ class DefaultClient implements Client {
334247

335248
// Once this is set, we don't defer any more callbacks.
336249
this.languageClient = languageClient;
337-
telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", getNonDefaultSettings(this.RootUri));
250+
this.settingsTracker = getTracker(this.RootUri);
251+
telemetry.logLanguageServerEvent("NonDefaultInitialCppSettings", this.settingsTracker.getUserModifiedSettings());
338252
failureMessageShown = false;
339253

340254
// Listen for messages from the language server.
@@ -447,13 +361,7 @@ class DefaultClient implements Client {
447361
}
448362

449363
public onDidChangeSettings(): void {
450-
// This relies on getNonDefaultSettings being called first.
451-
console.assert(Object.keys(previousCppSettings).length > 0);
452-
453-
let filter: (key: string, val: string) => boolean = (key: string, val: string) => {
454-
return !(key in previousCppSettings) || val !== previousCppSettings[key];
455-
};
456-
let changedSettings: { [key: string] : string} = collectSettingsForTelemetry(filter, this.RootUri);
364+
let changedSettings: { [key: string] : string} = this.settingsTracker.getChangedSettings();
457365

458366
if (Object.keys(changedSettings).length > 0) {
459367
if (changedSettings["commentContinuationPatterns"]) {

Extension/src/LanguageServer/configurations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as path from 'path';
88
import * as fs from "fs";
99
import * as vscode from 'vscode';
1010
import * as util from '../common';
11+
import * as telemetry from '../telemetry';
1112
import { PersistentFolderState } from './persistentState';
1213
import { CppSettings } from './settings';
1314
const configVersion: number = 4;
@@ -313,6 +314,7 @@ export class CppProperties {
313314

314315
public addToIncludePathCommand(path: string): void {
315316
this.handleConfigurationEditCommand((document: vscode.TextDocument) => {
317+
telemetry.logLanguageServerEvent("addToIncludePath");
316318
this.parsePropertiesFile(); // Clear out any modifications we may have made internally.
317319
let config: Configuration = this.configurationJson.configurations[this.CurrentConfiguration];
318320
if (config.includePath === undefined) {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
'use strict';
6+
7+
import * as vscode from 'vscode';
8+
import * as util from '../common';
9+
10+
/**
11+
* track settings changes for telemetry
12+
*/
13+
type FilterFunction = (key: string, val: string, settings: vscode.WorkspaceConfiguration) => boolean;
14+
type KeyValuePair = { key: string; value: string };
15+
16+
const maxSettingLengthForTelemetry: number = 50;
17+
let cache: SettingsTracker = undefined;
18+
19+
export class SettingsTracker {
20+
private previousCppSettings: { [key: string]: any } = {};
21+
private resource: vscode.Uri;
22+
23+
constructor(resource: vscode.Uri) {
24+
this.resource = resource;
25+
this.collectSettings(() => true);
26+
}
27+
28+
public getUserModifiedSettings(): { [key: string]: string } {
29+
let filter: FilterFunction = (key: string, val: string, settings: vscode.WorkspaceConfiguration) => {
30+
return !this.areEqual(val, settings.inspect(key).defaultValue);
31+
};
32+
return this.collectSettings(filter);
33+
}
34+
35+
public getChangedSettings(): { [key: string]: string } {
36+
let filter: FilterFunction = (key: string, val: string) => {
37+
return !(key in this.previousCppSettings) || !this.areEqual(val, this.previousCppSettings[key]);
38+
};
39+
return this.collectSettings(filter);
40+
}
41+
42+
private collectSettings(filter: FilterFunction): { [key: string]: string } {
43+
let settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", this.resource);
44+
let result: { [key: string]: string } = {};
45+
46+
for (let key in settings) {
47+
let val: any = this.getSetting(settings, key);
48+
if (val === undefined) {
49+
continue;
50+
}
51+
if (val instanceof Object && !(val instanceof Array)) {
52+
for (let subKey in val) {
53+
let newKey: string = key + "." + subKey;
54+
let subVal: any = this.getSetting(settings, newKey);
55+
if (subVal === undefined) {
56+
continue;
57+
}
58+
let entry: KeyValuePair = this.filterAndSanitize(newKey, subVal, settings, filter);
59+
if (entry && entry.key && entry.value) {
60+
result[entry.key] = entry.value;
61+
}
62+
}
63+
continue;
64+
}
65+
66+
let entry: KeyValuePair = this.filterAndSanitize(key, val, settings, filter);
67+
if (entry && entry.key && entry.value) {
68+
result[entry.key] = entry.value;
69+
}
70+
}
71+
72+
return result;
73+
}
74+
75+
private getSetting(settings: vscode.WorkspaceConfiguration, key: string): any {
76+
// Ignore methods and settings that don't exist
77+
if (settings.inspect(key).defaultValue !== undefined) {
78+
let val: any = settings.get(key);
79+
if (val instanceof Object) {
80+
return val; // It's a sub-section.
81+
}
82+
83+
// Only return values that match the setting's type and enum (if applicable).
84+
let curSetting: any = util.packageJson.contributes.configuration.properties["C_Cpp." + key];
85+
if (curSetting) {
86+
let type: string = this.typeMatch(val, curSetting["type"]);
87+
if (type) {
88+
if (type !== "string") {
89+
return val;
90+
}
91+
let curEnum: any[] = curSetting["enum"];
92+
if (curEnum && curEnum.indexOf(val) === -1) {
93+
return "<invalid>";
94+
}
95+
return val;
96+
}
97+
}
98+
}
99+
return undefined;
100+
}
101+
102+
private typeMatch(value: any, type?: string | string[]): string {
103+
if (type) {
104+
if (type instanceof Array) {
105+
for (let i: number = 0; i < type.length; i++) {
106+
let t: string = type[i];
107+
if (t) {
108+
if (typeof value === t) {
109+
return t;
110+
}
111+
if (t === "array" && value instanceof Array) {
112+
return t;
113+
}
114+
if (t === "null" && value === null) {
115+
return t;
116+
}
117+
}
118+
}
119+
} else if (typeof type === "string" && typeof value === type) {
120+
return type;
121+
}
122+
}
123+
return undefined;
124+
}
125+
126+
private filterAndSanitize(key: string, val: any, settings: vscode.WorkspaceConfiguration, filter: FilterFunction): KeyValuePair {
127+
if (filter(key, val, settings)) {
128+
let value: string;
129+
this.previousCppSettings[key] = val;
130+
switch (key) {
131+
case "clang_format_style":
132+
case "clang_format_fallbackStyle": {
133+
let newKey: string = key + "2";
134+
if (val) {
135+
switch (String(val).toLowerCase()) {
136+
case "visual studio":
137+
case "llvm":
138+
case "google":
139+
case "chromium":
140+
case "mozilla":
141+
case "webkit":
142+
case "file":
143+
case "none": {
144+
value = String(this.previousCppSettings[key]);
145+
break;
146+
}
147+
default: {
148+
value = "...";
149+
break;
150+
}
151+
}
152+
} else {
153+
value = "null";
154+
}
155+
key = newKey;
156+
break;
157+
}
158+
case "commentContinuationPatterns": {
159+
value = this.areEqual(val, settings.inspect(key).defaultValue) ? "<default>" : "..."; // Track whether it's being used, but nothing specific about it.
160+
break;
161+
}
162+
default: {
163+
if (key === "clang_format_path" || key.startsWith("default.")) {
164+
value = this.areEqual(val, settings.inspect(key).defaultValue) ? "<default>" : "..."; // Track whether it's being used, but nothing specific about it.
165+
} else {
166+
value = String(this.previousCppSettings[key]);
167+
}
168+
}
169+
}
170+
if (value && value.length > maxSettingLengthForTelemetry) {
171+
value = value.substr(0, maxSettingLengthForTelemetry) + "...";
172+
}
173+
return {key: key, value: value};
174+
}
175+
}
176+
177+
private areEqual(value1: any, value2: any): boolean {
178+
if (value1 instanceof Object && value2 instanceof Object) {
179+
return JSON.stringify(value1) === JSON.stringify(value2);
180+
}
181+
return value1 === value2;
182+
}
183+
}
184+
185+
export function getTracker(resource: vscode.Uri): SettingsTracker {
186+
if (!cache) {
187+
cache = new SettingsTracker(resource);
188+
}
189+
return cache;
190+
}

0 commit comments

Comments
 (0)