Repository: microsoft/vscode-edge-devtools
Pull request #406 (2021-06-03 23:59:12)

[Telemetry]: Adding extension settings related telemetry
This PR implements two telemetry events related to the extension settings:

- `user/settingsChangedAtLaunch`
    - This event fires upon extension activation and displays all extension settings that differ from the default value
    - Example: `user/settingsChangedAtLaunch: {"enableNetwork":"false","themes":"Light"}, undefined`
- `user/settingsChanged`
    - This event fires whenever a user changes a setting related to our extension
    - Example: `user/settingsChanged: {"enableNetwork":"false"}, undefined`

Going to try something new and do code explanation via comments. Marking them all as resolved so they are collapsible, but feel free to reply to and/or activate them if you have a question regarding that section.


Patch:
diff --git a/package.json b/package.json
index 9b6147a..1cafb45 100644
--- a/package.json
+++ b/package.json
@@ -176,6 +176,8 @@
                     "type": "object",
                     "description": "A set of mappings to override the locations of source map files.",
                     "default": {
+                        "webpack://./*": "${webRoot}/*",
+                        "webpack://*": "${webRoot}/*",
                         "webpack:///./*": "${webRoot}/*",
                         "webpack:///src/*": "${webRoot}/*",
                         "webpack:///*": "*",
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index 72bfaaf..f335769 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -342,7 +342,7 @@ export class DevToolsPanel {
                     style-src 'self' 'unsafe-inline' ${this.panel.webview.cspSource};
                     script-src 'self' 'unsafe-eval' ${this.panel.webview.cspSource};
                     frame-src 'self' ${this.panel.webview.cspSource};
-                    connect-src 'self' ${this.panel.webview.cspSource};
+                    connect-src 'self' data: ${this.panel.webview.cspSource};
                 ">
                 <meta name="referrer" content="no-referrer">
                 <link href="${stylesUri}" rel="stylesheet"/>
diff --git a/src/extension.ts b/src/extension.ts
index 95efef1..17dd307 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -28,6 +28,8 @@ import {
     getActiveDebugSessionId,
     getJsDebugCDPProxyWebsocketUrl,
     reportFileExtensionTypes,
+    reportChangedExtensionSetting,
+    reportExtensionSettings,
     reportUrlType,
 } from './utils';
 import { LaunchConfigManager } from './launchConfigManager';
@@ -166,6 +168,8 @@ export function activate(context: vscode.ExtensionContext): void {
         }));
     void vscode.commands.executeCommand('setContext', 'titleCommandsRegistered', true);
     void reportFileExtensionTypes(telemetryReporter);
+    reportExtensionSettings(telemetryReporter);
+    vscode.workspace.onDidChangeConfiguration(event => reportChangedExtensionSetting(event, telemetryReporter));
 }
 
 export async function attach(
diff --git a/src/utils.ts b/src/utils.ts
index 5e67e00..90c794f 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -615,6 +615,55 @@ async function verifyFlavorPath(flavor: BrowserFlavor | undefined, platform: Pla
     }
 }
 
+type ExtensionSettings = [string, boolean | string | {[key: string]: string} | undefined];
+
+export function reportExtensionSettings(telemetryReporter: Readonly<TelemetryReporter>): void {
+    const extensionSettingsList = Object.entries(vscode.workspace.getConfiguration(SETTINGS_STORE_NAME)).splice(4) as Array<ExtensionSettings>;
+    const settings = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME);
+    const changedSettingsMap: Map<string, string> = new Map<string, string>();
+    for (const currentSetting of extensionSettingsList) {
+        const settingName: string = currentSetting[0];
+        const settingValue: boolean | string | {[key: string]: string} | undefined = currentSetting[1];
+        const settingInspect = settings.inspect(settingName);
+        if (settingInspect) {
+            const defaultValue = settingInspect.defaultValue;
+            if (settingValue !== undefined && settingValue !== defaultValue) {
+                if (defaultValue && typeof defaultValue === 'object' && typeof settingValue === 'object') {
+                    for (const [key, value] of Object.entries(defaultValue)) {
+                        if (settingValue[key] !== value) {
+                            changedSettingsMap.set(settingName, JSON.stringify(settingValue));
+                            break;
+                        }
+                    }
+                } else {
+                    changedSettingsMap.set(settingName, settingValue.toString());
+                }
+            }
+        }
+    }
+    const changedSettingsObject = {};
+    Object.assign(changedSettingsObject, ...[...changedSettingsMap.entries()].map(([k, v]) => ({[k]: v})));
+    telemetryReporter.sendTelemetryEvent('user/settingsChangedAtLaunch', changedSettingsObject);
+}
+
+export function reportChangedExtensionSetting(event: vscode.ConfigurationChangeEvent, telemetryReporter: Readonly<TelemetryReporter>): void {
+    const extensionSettingsList = Object.entries(vscode.workspace.getConfiguration(SETTINGS_STORE_NAME)).splice(4) as Array<ExtensionSettings>;
+    for (const currentSetting of extensionSettingsList) {
+        const settingName: string = currentSetting[0];
+        const settingValue: boolean | string | {[key: string]: string} | undefined = currentSetting[1];
+        if (event.affectsConfiguration(`${SETTINGS_STORE_NAME}.${settingName}`)) {
+            if (settingName !== undefined) {
+                if (settingValue !== undefined) {
+                    const telemetryObject: {[key: string]: string}  = {};
+                    const objString = typeof settingValue !== 'object' ? settingValue.toString() : JSON.stringify(settingValue);
+                    telemetryObject[settingName] = objString;
+                    telemetryReporter.sendTelemetryEvent('user/settingsChanged', telemetryObject);
+                }
+            }
+        }
+    }
+}
+
 export function reportUrlType(url: string, telemetryReporter: Readonly<TelemetryReporter>): void {
     const localhostPattern = /^https?:\/\/localhost:/;
     const ipPattern = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/;


Test patch:
diff --git a/test/extension.test.ts b/test/extension.test.ts
index f9821df..33fb0f3 100644
--- a/test/extension.test.ts
+++ b/test/extension.test.ts
@@ -44,6 +44,8 @@ describe("extension", () => {
                 getJsDebugCDPProxyWebsocketUrl: jest.fn(),
                 getActiveDebugSessionId: jest.fn(),
                 reportFileExtensionTypes: jest.fn(),
+                reportChangedExtensionSetting: jest.fn(),
+                reportExtensionSettings: jest.fn(),
             };
             jest.doMock("../src/utils", () => mockUtils);
             jest.doMock("../src/launchDebugProvider");
@@ -402,6 +404,8 @@ describe("extension", () => {
                 removeTrailingSlash: jest.fn(removeTrailingSlash),
                 getJsDebugCDPProxyWebsocketUrl: jest.fn(),
                 buttonCode: { launch: '' },
+                reportChangedExtensionSetting: jest.fn(),
+                reportExtensionSettings: jest.fn(),
                 reportUrlType: jest.fn(),
                 reportFileExtensionTypes: jest.fn(),
             };
diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts
index e27d269..9aa7b33 100644
--- a/test/helpers/helpers.ts
+++ b/test/helpers/helpers.ts
@@ -71,20 +71,36 @@ export function createFakeVSCode() {
                 ];
             }),
             getConfiguration: jest.fn(() => {
-                return { get: (name: string) => {
-                    switch(name) {
-                        case "enableNetwork":
-                            return true;
-                        case "themeString":
-                            return "System preference";
-                        case "whatsNew":
-                            return true;
-                        case "isHeadless":
-                            return false;
-                        default:
-                            return undefined;
+                return {
+                    get: (name: string) => {
+                        switch(name) {
+                            case "enableNetwork":
+                                return true;
+                            case "themes":
+                                return "System preference";
+                            case "whatsNew":
+                                return true;
+                            case "isHeadless":
+                                return false;
+                            default:
+                                return undefined;
+                        }
+                    },
+                    inspect: (name: string) => {
+                        switch(name) {
+                            case "enableNetwork":
+                                return {defaultValue: true};
+                            case "themes":
+                                return {defaultValue: "Light"};
+                            case "whatsNew":
+                                return {defaultValue: false};
+                            case "isHeadless":
+                                return {defaultValue: false};
+                            default:
+                                return {defaultValue: undefined};
+                        }
                     }
-                } };
+                };
             }),
             onDidChangeConfiguration: jest.fn(),
             openTextDocument: jest.fn().mockResolvedValue(null),
diff --git a/test/utils.test.ts b/test/utils.test.ts
index 91e72bd..56b8be1 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -8,6 +8,7 @@ import * as path from "path";
 
 import { createFakeExtensionContext, createFakeGet, createFakeTelemetryReporter, createFakeVSCode, Mocked } from "./helpers/helpers";
 import { BrowserFlavor, IRemoteTargetJson, IUserConfig } from "../src/utils";
+import { ConfigurationChangeEvent } from "vscode";
 
 jest.mock("vscode", () => null, { virtual: true });
 
@@ -971,7 +972,7 @@ describe("utils", () => {
 
             for (let i = 0; i < input.length; i++) {
                 utils.reportUrlType(input[i], reporter);
-                expect(reporter.sendTelemetryEvent).toBeCalledWith('user/browserNavigation', { 'urlType': expected[i] })
+                expect(reporter.sendTelemetryEvent).toBeCalledWith('user/browserNavigation', { 'urlType': expected[i] });
             }
         });
     });
@@ -983,4 +984,45 @@ describe("utils", () => {
             expect(reporter.sendTelemetryEvent).toBeCalledWith('workspace/metadata', undefined, {"css": 1, "js": 1, "json": 1, "jsx": 1, "total": 4});
         });
     });
+
+    describe('reportExtensionSettings', () => {
+        let mockVSCode: typeof import("vscode");
+
+        beforeEach(() => {
+            const vscodeMock = jest.requireMock("vscode");
+            const originalWorkspaceMockConfig = vscodeMock.workspace.getConfiguration();
+            vscodeMock.workspace.getConfiguration.mockImplementation(() => {
+                return {
+                    ...originalWorkspaceMockConfig,
+                    fillerPropertyOne: 'Need to add filler values since the extension related telemetry functions removes the first 4 entries to create an array of extension settings',
+                    fillerPropertyTwo: 'The first 4 entries for a workspaceConfiguration object are class related functions, the rest are all extension related settings.',
+                    themes: 'System preference',
+                    whatsNew: 'true',
+                    enableNetwork: 'false',
+                }
+            });
+
+            jest.doMock("vscode", () => mockVSCode, { virtual: true });
+            jest.resetModules();
+        });
+
+        it('correctly records all changed extension settings', async () => {
+            const reporter = createFakeTelemetryReporter();
+            utils.reportExtensionSettings(reporter);
+            expect(reporter.sendTelemetryEvent).toBeCalledWith('user/settingsChangedAtLaunch', { themes: 'System preference', whatsNew: 'true', enableNetwork: 'false' });
+        });
+
+        it('correctly sends telemetry event for changed event', async () => {
+            const reporter = createFakeTelemetryReporter();
+            const configurationChangedEvent: ConfigurationChangeEvent = {affectsConfiguration: (name): boolean=> {
+                if (name === 'vscode-edge-devtools.enableNetwork') {
+                    return true;
+                } else {
+                    return false;
+                }
+            }};
+            utils.reportChangedExtensionSetting(configurationChangedEvent, reporter);
+            expect(reporter.sendTelemetryEvent).toBeCalledWith('user/settingsChanged', { enableNetwork: 'false' });
+        });
+    });
 });